diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-04-24 15:37:48 +0300 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-04-24 15:37:48 +0300 |
commit | 15a10da4e82c688b0cb1e266f4aebd477c979dce (patch) | |
tree | e6fef2fc87be5e03a26f554063b52695d4db2aba | |
parent | 90717c77b29c4a8ca0c3f49e9b4670b6fcbbe9d9 (diff) |
Gempub cover page; cleanup
Use MIME hooks to generate a Gempub cover page with a preloaded cover image.
This required applying MIME filtering to "file://" requests as well.
Todo: More cleanup, add a gempub.c.
-rw-r--r-- | src/gmrequest.c | 104 | ||||
-rw-r--r-- | src/gmutil.c | 71 | ||||
-rw-r--r-- | src/gmutil.h | 4 | ||||
-rw-r--r-- | src/mimehooks.c | 109 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 68 |
5 files changed, 281 insertions, 75 deletions
diff --git a/src/gmrequest.c b/src/gmrequest.c index 4a9cc763..8b87a638 100644 --- a/src/gmrequest.c +++ b/src/gmrequest.c | |||
@@ -148,7 +148,7 @@ iDefineAudienceGetter(GmRequest, updated) | |||
148 | iDefineAudienceGetter(GmRequest, finished) | 148 | iDefineAudienceGetter(GmRequest, finished) |
149 | 149 | ||
150 | static void checkServerCertificate_GmRequest_(iGmRequest *d) { | 150 | static void checkServerCertificate_GmRequest_(iGmRequest *d) { |
151 | const iTlsCertificate *cert = serverCertificate_TlsRequest(d->req); | 151 | const iTlsCertificate *cert = d->req ? serverCertificate_TlsRequest(d->req) : NULL; |
152 | iGmResponse *resp = d->resp; | 152 | iGmResponse *resp = d->resp; |
153 | resp->certFlags = 0; | 153 | resp->certFlags = 0; |
154 | if (cert) { | 154 | if (cert) { |
@@ -256,6 +256,20 @@ static void readIncoming_GmRequest_(iGmRequest *d, iTlsRequest *req) { | |||
256 | } | 256 | } |
257 | } | 257 | } |
258 | 258 | ||
259 | static void applyFilter_GmRequest_(iGmRequest *d) { | ||
260 | iAssert(d->state == finished_GmRequestState); | ||
261 | iBlock *xbody = tryFilter_MimeHooks(mimeHooks_App(), &d->resp->meta, &d->resp->body, &d->url); | ||
262 | if (xbody) { | ||
263 | lock_Mutex(d->mtx); | ||
264 | clear_String(&d->resp->meta); | ||
265 | clear_Block(&d->resp->body); | ||
266 | d->state = receivingHeader_GmRequestState; | ||
267 | processIncomingData_GmRequest_(d, xbody); | ||
268 | d->state = finished_GmRequestState; | ||
269 | unlock_Mutex(d->mtx); | ||
270 | } | ||
271 | } | ||
272 | |||
259 | static void requestFinished_GmRequest_(iGmRequest *d, iTlsRequest *req) { | 273 | static void requestFinished_GmRequest_(iGmRequest *d, iTlsRequest *req) { |
260 | iAssert(req == d->req); | 274 | iAssert(req == d->req); |
261 | lock_Mutex(d->mtx); | 275 | lock_Mutex(d->mtx); |
@@ -275,17 +289,7 @@ static void requestFinished_GmRequest_(iGmRequest *d, iTlsRequest *req) { | |||
275 | unlock_Mutex(d->mtx); | 289 | unlock_Mutex(d->mtx); |
276 | /* Check for mimehooks. */ | 290 | /* Check for mimehooks. */ |
277 | if (d->isRespFiltered && d->state == finished_GmRequestState) { | 291 | if (d->isRespFiltered && d->state == finished_GmRequestState) { |
278 | iBlock *xbody = | 292 | applyFilter_GmRequest_(d); |
279 | tryFilter_MimeHooks(mimeHooks_App(), &d->resp->meta, &d->resp->body, &d->url); | ||
280 | if (xbody) { | ||
281 | lock_Mutex(d->mtx); | ||
282 | clear_String(&d->resp->meta); | ||
283 | clear_Block(&d->resp->body); | ||
284 | d->state = receivingHeader_GmRequestState; | ||
285 | processIncomingData_GmRequest_(d, xbody); | ||
286 | d->state = finished_GmRequestState; | ||
287 | unlock_Mutex(d->mtx); | ||
288 | } | ||
289 | } | 293 | } |
290 | iNotifyAudience(d, finished, GmRequestFinished); | 294 | iNotifyAudience(d, finished, GmRequestFinished); |
291 | } | 295 | } |
@@ -531,7 +535,8 @@ static const iString *findContainerArchive_(const iString *path) { | |||
531 | iBeginCollect(); | 535 | iBeginCollect(); |
532 | while (!isEmpty_String(path) && cmp_String(path, ".")) { | 536 | while (!isEmpty_String(path) && cmp_String(path, ".")) { |
533 | iString *dir = newRange_String(dirName_Path(path)); | 537 | iString *dir = newRange_String(dirName_Path(path)); |
534 | if (endsWithCase_String(dir, ".zip")) { | 538 | if (endsWithCase_String(dir, ".zip") || |
539 | endsWithCase_String(dir, ".gpub")) { | ||
535 | iEndCollect(); | 540 | iEndCollect(); |
536 | return collect_String(dir); | 541 | return collect_String(dir); |
537 | } | 542 | } |
@@ -541,49 +546,6 @@ static const iString *findContainerArchive_(const iString *path) { | |||
541 | return NULL; | 546 | return NULL; |
542 | } | 547 | } |
543 | 548 | ||
544 | static const char *mediaTypeFromPath_(const iString *path) { | ||
545 | if (endsWithCase_String(path, ".gmi") || endsWithCase_String(path, ".gemini")) { | ||
546 | return "text/gemini; charset=utf-8"; | ||
547 | } | ||
548 | /* TODO: It would be better to default to text/plain, but switch to | ||
549 | application/octet-stream if the contents fail to parse as UTF-8. */ | ||
550 | else if (endsWithCase_String(path, ".txt") || | ||
551 | endsWithCase_String(path, ".md") || | ||
552 | endsWithCase_String(path, ".c") || | ||
553 | endsWithCase_String(path, ".h") || | ||
554 | endsWithCase_String(path, ".cc") || | ||
555 | endsWithCase_String(path, ".hh") || | ||
556 | endsWithCase_String(path, ".cpp") || | ||
557 | endsWithCase_String(path, ".hpp")) { | ||
558 | return "text/plain"; | ||
559 | } | ||
560 | else if (endsWithCase_String(path, ".zip")) { | ||
561 | return "application/zip"; | ||
562 | } | ||
563 | else if (endsWithCase_String(path, ".png")) { | ||
564 | return "image/png"; | ||
565 | } | ||
566 | else if (endsWithCase_String(path, ".jpg") || endsWithCase_String(path, ".jpeg")) { | ||
567 | return "image/jpeg"; | ||
568 | } | ||
569 | else if (endsWithCase_String(path, ".gif")) { | ||
570 | return "image/gif"; | ||
571 | } | ||
572 | else if (endsWithCase_String(path, ".wav")) { | ||
573 | return "audio/wave"; | ||
574 | } | ||
575 | else if (endsWithCase_String(path, ".ogg")) { | ||
576 | return "audio/ogg"; | ||
577 | } | ||
578 | else if (endsWithCase_String(path, ".mp3")) { | ||
579 | return "audio/mpeg"; | ||
580 | } | ||
581 | else if (endsWithCase_String(path, ".mid")) { | ||
582 | return "audio/midi"; | ||
583 | } | ||
584 | return "application/octet-stream"; | ||
585 | } | ||
586 | |||
587 | static iBool isDirectory_(const iString *path) { | 549 | static iBool isDirectory_(const iString *path) { |
588 | /* TODO: move this to the_Foundation */ | 550 | /* TODO: move this to the_Foundation */ |
589 | iFileInfo *info = new_FileInfo(path); | 551 | iFileInfo *info = new_FileInfo(path); |
@@ -644,14 +606,8 @@ void submit_GmRequest(iGmRequest *d) { | |||
644 | return; | 606 | return; |
645 | } | 607 | } |
646 | else if (equalCase_Rangecc(url.scheme, "file")) { | 608 | else if (equalCase_Rangecc(url.scheme, "file")) { |
647 | /* TODO: Move this elsewhere. */ | 609 | /* TODO: Move handling of "file://" URLs elsewhere, it's getting complex. */ |
648 | iString *path = collect_String(urlDecode_String(collect_String(newRange_String(url.path)))); | 610 | iString *path = collect_String(localFilePathFromUrl_String(&d->url)); |
649 | #if defined (iPlatformMsys) | ||
650 | /* Remove the extra slash from the beginning. */ | ||
651 | if (startsWith_String(path, "/")) { | ||
652 | remove_Block(&path->chars, 0, 1); | ||
653 | } | ||
654 | #endif | ||
655 | iFile *f = new_File(path); | 611 | iFile *f = new_File(path); |
656 | if (isDirectory_(path)) { | 612 | if (isDirectory_(path)) { |
657 | if (endsWith_String(path, "/")) { | 613 | if (endsWith_String(path, "/")) { |
@@ -669,6 +625,14 @@ void submit_GmRequest(iGmRequest *d) { | |||
669 | iPtrArray *sortedInfo = collectNew_PtrArray(); | 625 | iPtrArray *sortedInfo = collectNew_PtrArray(); |
670 | iForEach(DirFileInfo, entry, | 626 | iForEach(DirFileInfo, entry, |
671 | iClob(directoryContents_FileInfo(iClob(new_FileInfo(path))))) { | 627 | iClob(directoryContents_FileInfo(iClob(new_FileInfo(path))))) { |
628 | /* Ignore some files. */ | ||
629 | #if defined (iPlatformApple) | ||
630 | const iRangecc name = baseName_Path(path_FileInfo(entry.value)); | ||
631 | if (equal_Rangecc(name, ".DS_Store") || | ||
632 | equal_Rangecc(name, ".localized")) { | ||
633 | continue; | ||
634 | } | ||
635 | #endif | ||
672 | pushBack_PtrArray(sortedInfo, ref_Object(entry.value)); | 636 | pushBack_PtrArray(sortedInfo, ref_Object(entry.value)); |
673 | } | 637 | } |
674 | sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_); | 638 | sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_); |
@@ -684,7 +648,7 @@ void submit_GmRequest(iGmRequest *d) { | |||
684 | } | 648 | } |
685 | else if (open_File(f, readOnly_FileMode)) { | 649 | else if (open_File(f, readOnly_FileMode)) { |
686 | resp->statusCode = success_GmStatusCode; | 650 | resp->statusCode = success_GmStatusCode; |
687 | setCStr_String(&resp->meta, mediaTypeFromPath_(path)); | 651 | setCStr_String(&resp->meta, mediaTypeFromPath_String(path)); |
688 | /* TODO: Detect text files based on contents? E.g., is the content valid UTF-8. */ | 652 | /* TODO: Detect text files based on contents? E.g., is the content valid UTF-8. */ |
689 | set_Block(&resp->body, collect_Block(readAll_File(f))); | 653 | set_Block(&resp->body, collect_Block(readAll_File(f))); |
690 | d->state = receivingBody_GmRequestState; | 654 | d->state = receivingBody_GmRequestState; |
@@ -718,6 +682,7 @@ void submit_GmRequest(iGmRequest *d) { | |||
718 | } | 682 | } |
719 | } | 683 | } |
720 | /* Show an archive index page if this is a directory. */ | 684 | /* Show an archive index page if this is a directory. */ |
685 | /* TODO: Use a built-in MIME hook for this? */ | ||
721 | if (isDir) { | 686 | if (isDir) { |
722 | iString *page = new_String(); | 687 | iString *page = new_String(); |
723 | const iRangecc containerName = baseName_Path(container); | 688 | const iRangecc containerName = baseName_Path(container); |
@@ -779,7 +744,7 @@ void submit_GmRequest(iGmRequest *d) { | |||
779 | const iBlock *data = data_Archive(arch, entryPath); | 744 | const iBlock *data = data_Archive(arch, entryPath); |
780 | if (data) { | 745 | if (data) { |
781 | resp->statusCode = success_GmStatusCode; | 746 | resp->statusCode = success_GmStatusCode; |
782 | setCStr_String(&resp->meta, mediaTypeFromPath_(entryPath)); | 747 | setCStr_String(&resp->meta, mediaTypeFromPath_String(entryPath)); |
783 | set_Block(&resp->body, data); | 748 | set_Block(&resp->body, data); |
784 | } | 749 | } |
785 | else { | 750 | else { |
@@ -787,6 +752,7 @@ void submit_GmRequest(iGmRequest *d) { | |||
787 | setCStr_String(&resp->meta, cstr_String(path)); | 752 | setCStr_String(&resp->meta, cstr_String(path)); |
788 | } | 753 | } |
789 | } | 754 | } |
755 | fileRequestFinished:; | ||
790 | } | 756 | } |
791 | } | 757 | } |
792 | else { | 758 | else { |
@@ -794,9 +760,13 @@ void submit_GmRequest(iGmRequest *d) { | |||
794 | setCStr_String(&resp->meta, cstr_String(path)); | 760 | setCStr_String(&resp->meta, cstr_String(path)); |
795 | } | 761 | } |
796 | } | 762 | } |
797 | fileRequestFinished: | ||
798 | iRelease(f); | 763 | iRelease(f); |
799 | d->state = finished_GmRequestState; | 764 | d->state = finished_GmRequestState; |
765 | /* MIME hooks may to this content. */ | ||
766 | if (d->isFilterEnabled && resp->statusCode == success_GmStatusCode) { | ||
767 | /* TODO: Use a background thread, the hook may take some time to run. */ | ||
768 | applyFilter_GmRequest_(d); | ||
769 | } | ||
800 | iNotifyAudience(d, finished, GmRequestFinished); | 770 | iNotifyAudience(d, finished, GmRequestFinished); |
801 | return; | 771 | return; |
802 | } | 772 | } |
diff --git a/src/gmutil.c b/src/gmutil.c index 18130951..806c989c 100644 --- a/src/gmutil.c +++ b/src/gmutil.c | |||
@@ -422,6 +422,71 @@ const char *makeFileUrl_CStr(const char *localFilePath) { | |||
422 | return cstrCollect_String(makeFileUrl_String(collectNewCStr_String(localFilePath))); | 422 | return cstrCollect_String(makeFileUrl_String(collectNewCStr_String(localFilePath))); |
423 | } | 423 | } |
424 | 424 | ||
425 | iString *localFilePathFromUrl_String(const iString *d) { | ||
426 | iUrl url; | ||
427 | init_Url(&url, d); | ||
428 | if (!equalCase_Rangecc(url.scheme, "file")) { | ||
429 | return NULL; | ||
430 | } | ||
431 | iString *path = urlDecode_String(collect_String(newRange_String(url.path))); | ||
432 | #if defined (iPlatformMsys) | ||
433 | /* Remove the extra slash from the beginning. */ | ||
434 | if (startsWith_String(path, "/")) { | ||
435 | remove_Block(&path->chars, 0, 1); | ||
436 | } | ||
437 | #endif | ||
438 | return path; | ||
439 | } | ||
440 | |||
441 | const char *mediaTypeFromPath_String(const iString *path) { | ||
442 | if (endsWithCase_String(path, ".gmi") || endsWithCase_String(path, ".gemini")) { | ||
443 | return "text/gemini; charset=utf-8"; | ||
444 | } | ||
445 | /* TODO: It would be better to default to text/plain, but switch to | ||
446 | application/octet-stream if the contents fail to parse as UTF-8. */ | ||
447 | else if (endsWithCase_String(path, ".txt") || | ||
448 | endsWithCase_String(path, ".md") || | ||
449 | endsWithCase_String(path, ".c") || | ||
450 | endsWithCase_String(path, ".h") || | ||
451 | endsWithCase_String(path, ".cc") || | ||
452 | endsWithCase_String(path, ".hh") || | ||
453 | endsWithCase_String(path, ".cpp") || | ||
454 | endsWithCase_String(path, ".hpp")) { | ||
455 | return "text/plain"; | ||
456 | } | ||
457 | else if (endsWithCase_String(path, ".zip")) { | ||
458 | return "application/zip"; | ||
459 | } | ||
460 | else if (endsWithCase_String(path, ".gpub")) { | ||
461 | return "application/gpub+zip"; | ||
462 | } | ||
463 | else if (endsWithCase_String(path, ".xml")) { | ||
464 | return "text/xml"; | ||
465 | } | ||
466 | else if (endsWithCase_String(path, ".png")) { | ||
467 | return "image/png"; | ||
468 | } | ||
469 | else if (endsWithCase_String(path, ".jpg") || endsWithCase_String(path, ".jpeg")) { | ||
470 | return "image/jpeg"; | ||
471 | } | ||
472 | else if (endsWithCase_String(path, ".gif")) { | ||
473 | return "image/gif"; | ||
474 | } | ||
475 | else if (endsWithCase_String(path, ".wav")) { | ||
476 | return "audio/wave"; | ||
477 | } | ||
478 | else if (endsWithCase_String(path, ".ogg")) { | ||
479 | return "audio/ogg"; | ||
480 | } | ||
481 | else if (endsWithCase_String(path, ".mp3")) { | ||
482 | return "audio/mpeg"; | ||
483 | } | ||
484 | else if (endsWithCase_String(path, ".mid")) { | ||
485 | return "audio/midi"; | ||
486 | } | ||
487 | return "application/octet-stream"; | ||
488 | } | ||
489 | |||
425 | void urlEncodeSpaces_String(iString *d) { | 490 | void urlEncodeSpaces_String(iString *d) { |
426 | for (;;) { | 491 | for (;;) { |
427 | const size_t pos = indexOfCStr_String(d, " "); | 492 | const size_t pos = indexOfCStr_String(d, " "); |
@@ -440,6 +505,12 @@ const iString *withSpacesEncoded_String(const iString *d) { | |||
440 | return collect_String(enc); | 505 | return collect_String(enc); |
441 | } | 506 | } |
442 | 507 | ||
508 | iRangecc mediaTypeWithoutParameters_Rangecc(iRangecc mime) { | ||
509 | iRangecc part = iNullRange; | ||
510 | nextSplit_Rangecc(mime, ";", &part); | ||
511 | return part; | ||
512 | } | ||
513 | |||
443 | const iString *feedEntryOpenCommand_String(const iString *url, int newTab) { | 514 | const iString *feedEntryOpenCommand_String(const iString *url, int newTab) { |
444 | if (!isEmpty_String(url)) { | 515 | if (!isEmpty_String(url)) { |
445 | iString *cmd = collectNew_String(); | 516 | iString *cmd = collectNew_String(); |
diff --git a/src/gmutil.h b/src/gmutil.h index 64c015b8..7aedbc47 100644 --- a/src/gmutil.h +++ b/src/gmutil.h | |||
@@ -115,7 +115,11 @@ void urlDecodePath_String (iString *); | |||
115 | void urlEncodePath_String (iString *); | 115 | void urlEncodePath_String (iString *); |
116 | iString * makeFileUrl_String (const iString *localFilePath); | 116 | iString * makeFileUrl_String (const iString *localFilePath); |
117 | const char * makeFileUrl_CStr (const char *localFilePath); | 117 | const char * makeFileUrl_CStr (const char *localFilePath); |
118 | iString * localFilePathFromUrl_String(const iString *); | ||
118 | void urlEncodeSpaces_String (iString *); | 119 | void urlEncodeSpaces_String (iString *); |
119 | const iString * withSpacesEncoded_String(const iString *); | 120 | const iString * withSpacesEncoded_String(const iString *); |
120 | 121 | ||
122 | const char * mediaTypeFromPath_String (const iString *path); | ||
123 | iRangecc mediaTypeWithoutParameters_Rangecc (iRangecc mime); | ||
124 | |||
121 | const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */ | 125 | const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */ |
diff --git a/src/mimehooks.c b/src/mimehooks.c index 3b3c0d1a..04cbcb36 100644 --- a/src/mimehooks.c +++ b/src/mimehooks.c | |||
@@ -1,6 +1,9 @@ | |||
1 | #include "mimehooks.h" | 1 | #include "mimehooks.h" |
2 | #include "defs.h" | ||
3 | #include "gmutil.h" | ||
2 | #include "app.h" | 4 | #include "app.h" |
3 | 5 | ||
6 | #include <the_Foundation/archive.h> | ||
4 | #include <the_Foundation/file.h> | 7 | #include <the_Foundation/file.h> |
5 | #include <the_Foundation/fileinfo.h> | 8 | #include <the_Foundation/fileinfo.h> |
6 | #include <the_Foundation/path.h> | 9 | #include <the_Foundation/path.h> |
@@ -109,7 +112,8 @@ static iBlock *translateAtomXmlToGeminiFeed_(const iString *mime, const iBlock * | |||
109 | init_String(&out); | 112 | init_String(&out); |
110 | format_String(&out, | 113 | format_String(&out, |
111 | "20 text/gemini\r\n" | 114 | "20 text/gemini\r\n" |
112 | "# %s\n\n", cstr_String(title)); | 115 | "# %s\n\n", |
116 | cstr_String(title)); | ||
113 | if (!isEmpty_String(subtitle)) { | 117 | if (!isEmpty_String(subtitle)) { |
114 | appendFormat_String(&out, "## %s\n\n", cstr_String(subtitle)); | 118 | appendFormat_String(&out, "## %s\n\n", cstr_String(subtitle)); |
115 | } | 119 | } |
@@ -177,8 +181,97 @@ finished: | |||
177 | return output; | 181 | return output; |
178 | } | 182 | } |
179 | 183 | ||
184 | static void appendGemPubProperty_(iString *out, const char *key, const iString *value) { | ||
185 | if (!isEmpty_String(value)) { | ||
186 | appendFormat_String(out, "%s: %s\n", key, cstr_String(value)); | ||
187 | } | ||
188 | } | ||
189 | |||
190 | iBlock *translateGemPubCoverPage_(const iString *mime, const iBlock *source, | ||
191 | const iString *requestUrl) { | ||
192 | iBlock *output = NULL; | ||
193 | iArchive *arch = new_Archive(); | ||
194 | if (openData_Archive(arch, source)) { | ||
195 | /* Parse the metadata and check if the required contents are present. */ | ||
196 | const iBlock *metadata = dataCStr_Archive(arch, "metadata"); | ||
197 | if (!metadata) { | ||
198 | goto cleanup; | ||
199 | } | ||
200 | enum iGemPubProperty { | ||
201 | title_GemPubProperty, | ||
202 | author_GemPubProperty, | ||
203 | lang_GemPubProperty, | ||
204 | description_GemPubProperty, | ||
205 | pubDate_GemPubProperty, | ||
206 | revDate_GemPubProperty, | ||
207 | version_GemPubProperty, | ||
208 | cover_GemPubProperty, | ||
209 | max_GemPubProperty | ||
210 | }; | ||
211 | static const char *labels[max_GemPubProperty] = { | ||
212 | "title:", | ||
213 | "author:", | ||
214 | "language:", | ||
215 | "description:", | ||
216 | "publishDate:", | ||
217 | "revisionDate:", | ||
218 | "version:", | ||
219 | "cover:" | ||
220 | }; | ||
221 | iString *props[max_GemPubProperty]; | ||
222 | iForIndices(i, props) { | ||
223 | props[i] = collectNew_String(); | ||
224 | } | ||
225 | /* Default values. */ | ||
226 | setCStr_String(props[title_GemPubProperty], "Untitled Book"); | ||
227 | setCStr_String(props[cover_GemPubProperty], | ||
228 | entryCStr_Archive(arch, "cover.jpg") ? "cover.jpg" : | ||
229 | entryCStr_Archive(arch, "cover.png") ? "cover.png" : ""); | ||
230 | iRangecc line = iNullRange; | ||
231 | while (nextSplit_Rangecc(range_Block(metadata), "\n", &line)) { | ||
232 | iRangecc clean = line; | ||
233 | trim_Rangecc(&clean); | ||
234 | iForIndices(i, props) { | ||
235 | if (startsWithCase_Rangecc(clean, labels[i])) { | ||
236 | setRange_String(props[i], (iRangecc){ clean.start + strlen(labels[i]), clean.end }); | ||
237 | trim_String(props[i]); | ||
238 | } | ||
239 | } | ||
240 | } | ||
241 | const iString *baseUrl = withSpacesEncoded_String(requestUrl); | ||
242 | iString *out = new_String(); | ||
243 | format_String(out, "20 text/gemini; charset=utf-8\r\n" | ||
244 | "# %s\n", | ||
245 | cstr_String(props[title_GemPubProperty])); | ||
246 | if (!isEmpty_String(props[description_GemPubProperty])) { | ||
247 | appendFormat_String(out, "%s\n", cstr_String(props[description_GemPubProperty])); | ||
248 | } | ||
249 | appendCStr_String(out, "\n"); | ||
250 | appendGemPubProperty_(out, "Author", props[author_GemPubProperty]); | ||
251 | appendGemPubProperty_(out, "Version", props[version_GemPubProperty]); | ||
252 | appendFormat_String(out, "\n=> %s/capsule/ " book_Icon " Book index page\n", cstr_String(baseUrl)); | ||
253 | if (!isEmpty_String(props[cover_GemPubProperty])) { | ||
254 | appendFormat_String(out, "\n=> %s/%s Cover image\n", | ||
255 | cstr_String(baseUrl), | ||
256 | cstr_String(props[cover_GemPubProperty])); | ||
257 | } | ||
258 | appendCStr_String(out, "\n## About this book\n"); | ||
259 | appendGemPubProperty_(out, "Revision date", props[revDate_GemPubProperty]); | ||
260 | appendGemPubProperty_(out, "Publish date", props[pubDate_GemPubProperty]); | ||
261 | appendGemPubProperty_(out, "Language", props[lang_GemPubProperty]); | ||
262 | output = copy_Block(utf8_String(out)); | ||
263 | delete_String(out); | ||
264 | } | ||
265 | cleanup: | ||
266 | iRelease(arch); | ||
267 | return output; | ||
268 | } | ||
269 | |||
180 | /*----------------------------------------------------------------------------------------------*/ | 270 | /*----------------------------------------------------------------------------------------------*/ |
181 | 271 | ||
272 | static const char *gpubMimeType_MimeHooks_ = "application/gpub+zip"; | ||
273 | static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt"; | ||
274 | |||
182 | struct Impl_MimeHooks { | 275 | struct Impl_MimeHooks { |
183 | iPtrArray filters; | 276 | iPtrArray filters; |
184 | }; | 277 | }; |
@@ -196,6 +289,12 @@ void deinit_MimeHooks(iMimeHooks *d) { | |||
196 | deinit_PtrArray(&d->filters); | 289 | deinit_PtrArray(&d->filters); |
197 | } | 290 | } |
198 | 291 | ||
292 | static iBool checkGemPub_(const iString *mime, const iString *requestUrl) { | ||
293 | /* Only process GemPub in local files. */ | ||
294 | return (equalCase_Rangecc(urlScheme_String(requestUrl), "file") && | ||
295 | startsWithCase_String(mime, gpubMimeType_MimeHooks_)); | ||
296 | } | ||
297 | |||
199 | iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) { | 298 | iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) { |
200 | /* TODO: Combine this function with tryFilter_MimeHooks! */ | 299 | /* TODO: Combine this function with tryFilter_MimeHooks! */ |
201 | iRegExpMatch m; | 300 | iRegExpMatch m; |
@@ -228,6 +327,12 @@ iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlo | |||
228 | } | 327 | } |
229 | } | 328 | } |
230 | /* Built-in filters. */ | 329 | /* Built-in filters. */ |
330 | if (checkGemPub_(mime, requestUrl)) { | ||
331 | iBlock *result = translateGemPubCoverPage_(mime, body, requestUrl); | ||
332 | if (result) { | ||
333 | return result; | ||
334 | } | ||
335 | } | ||
231 | init_RegExpMatch(&m); | 336 | init_RegExpMatch(&m); |
232 | if (matchString_RegExp(xmlMimePattern_(), mime, &m)) { | 337 | if (matchString_RegExp(xmlMimePattern_(), mime, &m)) { |
233 | iBlock *result = translateAtomXmlToGeminiFeed_(mime, body, requestUrl); | 338 | iBlock *result = translateAtomXmlToGeminiFeed_(mime, body, requestUrl); |
@@ -238,8 +343,6 @@ iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlo | |||
238 | return NULL; | 343 | return NULL; |
239 | } | 344 | } |
240 | 345 | ||
241 | static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt"; | ||
242 | |||
243 | void load_MimeHooks(iMimeHooks *d, const char *saveDir) { | 346 | void load_MimeHooks(iMimeHooks *d, const char *saveDir) { |
244 | iBool reportError = iFalse; | 347 | iBool reportError = iFalse; |
245 | iFile *f = newCStr_File(concatPath_CStr(saveDir, mimeHooksFilename_MimeHooks_)); | 348 | iFile *f = newCStr_File(concatPath_CStr(saveDir, mimeHooksFilename_MimeHooks_)); |
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 9b414b63..11baf9ee 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c | |||
@@ -54,6 +54,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
54 | # include "ios.h" | 54 | # include "ios.h" |
55 | #endif | 55 | #endif |
56 | 56 | ||
57 | #include <the_Foundation/archive.h> | ||
57 | #include <the_Foundation/file.h> | 58 | #include <the_Foundation/file.h> |
58 | #include <the_Foundation/fileinfo.h> | 59 | #include <the_Foundation/fileinfo.h> |
59 | #include <the_Foundation/objectlist.h> | 60 | #include <the_Foundation/objectlist.h> |
@@ -1038,6 +1039,22 @@ static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) { | |||
1038 | } | 1039 | } |
1039 | } | 1040 | } |
1040 | 1041 | ||
1042 | static const char *zipPageHeading_(const iRangecc mime) { | ||
1043 | if (equalCase_Rangecc(mime, "application/gpub+zip")) { | ||
1044 | return book_Icon " Gempub Book"; | ||
1045 | } | ||
1046 | iRangecc type = iNullRange; | ||
1047 | nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */ | ||
1048 | nextSplit_Rangecc(mime, "/", &type); | ||
1049 | if (startsWithCase_Rangecc(type, "x-")) { | ||
1050 | type.start += 2; | ||
1051 | } | ||
1052 | iString *heading = upper_String(collectNewRange_String(type)); | ||
1053 | appendCStr_String(heading, " Archive"); | ||
1054 | prependCStr_String(heading, folder_Icon " "); | ||
1055 | return cstrCollect_String(heading); | ||
1056 | } | ||
1057 | |||
1041 | static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response, | 1058 | static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response, |
1042 | const iBool isInitialUpdate) { | 1059 | const iBool isInitialUpdate) { |
1043 | if (d->state == ready_RequestState) { | 1060 | if (d->state == ready_RequestState) { |
@@ -1078,13 +1095,16 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
1078 | docFormat = plainText_GmDocumentFormat; | 1095 | docFormat = plainText_GmDocumentFormat; |
1079 | setRange_String(&d->sourceMime, param); | 1096 | setRange_String(&d->sourceMime, param); |
1080 | } | 1097 | } |
1081 | else if (equal_Rangecc(param, "application/zip")) { | 1098 | else if (equal_Rangecc(param, "application/zip") || |
1099 | (startsWith_Rangecc(param, "application/") && | ||
1100 | endsWithCase_Rangecc(param, "+zip"))) { | ||
1082 | docFormat = gemini_GmDocumentFormat; | 1101 | docFormat = gemini_GmDocumentFormat; |
1083 | setRange_String(&d->sourceMime, param); | 1102 | setRange_String(&d->sourceMime, param); |
1084 | iString *key = collectNew_String(); | 1103 | iString *key = collectNew_String(); |
1085 | toString_Sym(SDLK_s, KMOD_PRIMARY, key); | 1104 | toString_Sym(SDLK_s, KMOD_PRIMARY, key); |
1086 | format_String(&str, "# " folder_Icon " ZIP Archive\n" | 1105 | format_String(&str, "# %s\n" |
1087 | "%s is a ZIP archive.\n\n%s\n\n", | 1106 | "%s is a compressed archive.\n\n%s\n\n", |
1107 | zipPageHeading_(param), | ||
1088 | cstr_Rangecc(baseName_Path(d->mod.url)), | 1108 | cstr_Rangecc(baseName_Path(d->mod.url)), |
1089 | format_CStr(cstr_Lang("error.unsupported.suggestsave"), | 1109 | format_CStr(cstr_Lang("error.unsupported.suggestsave"), |
1090 | cstr_String(key), | 1110 | cstr_String(key), |
@@ -1101,6 +1121,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
1101 | /* Make a simple document with an image or audio player. */ | 1121 | /* Make a simple document with an image or audio player. */ |
1102 | docFormat = gemini_GmDocumentFormat; | 1122 | docFormat = gemini_GmDocumentFormat; |
1103 | setRange_String(&d->sourceMime, param); | 1123 | setRange_String(&d->sourceMime, param); |
1124 | const iGmLinkId imgLinkId = 1; /* there's only the one link */ | ||
1104 | if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) { | 1125 | if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) { |
1105 | const char *linkTitle = | 1126 | const char *linkTitle = |
1106 | startsWith_String(mimeStr, "image/") ? "Image" : "Audio"; | 1127 | startsWith_String(mimeStr, "image/") ? "Image" : "Audio"; |
@@ -1112,7 +1133,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
1112 | } | 1133 | } |
1113 | format_String(&str, "=> %s %s\n", cstr_String(d->mod.url), linkTitle); | 1134 | format_String(&str, "=> %s %s\n", cstr_String(d->mod.url), linkTitle); |
1114 | setData_Media(media_GmDocument(d->doc), | 1135 | setData_Media(media_GmDocument(d->doc), |
1115 | 1, | 1136 | imgLinkId, |
1116 | mimeStr, | 1137 | mimeStr, |
1117 | &response->body, | 1138 | &response->body, |
1118 | !isRequestFinished ? partialData_MediaFlag : 0); | 1139 | !isRequestFinished ? partialData_MediaFlag : 0); |
@@ -1121,7 +1142,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
1121 | else if (isAudio && !isInitialUpdate) { | 1142 | else if (isAudio && !isInitialUpdate) { |
1122 | /* Update the audio content. */ | 1143 | /* Update the audio content. */ |
1123 | setData_Media(media_GmDocument(d->doc), | 1144 | setData_Media(media_GmDocument(d->doc), |
1124 | 1, | 1145 | imgLinkId, |
1125 | mimeStr, | 1146 | mimeStr, |
1126 | &response->body, | 1147 | &response->body, |
1127 | !isRequestFinished ? partialData_MediaFlag : 0); | 1148 | !isRequestFinished ? partialData_MediaFlag : 0); |
@@ -1157,6 +1178,43 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
1157 | if (setSource) { | 1178 | if (setSource) { |
1158 | setSource_DocumentWidget(d, &str); | 1179 | setSource_DocumentWidget(d, &str); |
1159 | } | 1180 | } |
1181 | if (isRequestFinished) { | ||
1182 | /* Gempub: Preload cover image. */ { | ||
1183 | /* TODO: move to a gempub.c along with other related code */ | ||
1184 | iString *localPath = localFilePathFromUrl_String(d->mod.url); | ||
1185 | if (localPath) { | ||
1186 | if (!iCmpStr(mediaTypeFromPath_String(localPath), "application/gpub+zip")) { | ||
1187 | iArchive *arch = iClob(new_Archive()); | ||
1188 | if (openFile_Archive(arch, localPath)) { | ||
1189 | iBool haveImage = iFalse; | ||
1190 | for (size_t linkId = 1; ; linkId++) { | ||
1191 | const iString *linkUrl = linkUrl_GmDocument(d->doc, linkId); | ||
1192 | if (!linkUrl) break; | ||
1193 | if (findLinkImage_Media(media_GmDocument(d->doc), linkId)) { | ||
1194 | continue; /* got this already */ | ||
1195 | } | ||
1196 | if (linkFlags_GmDocument(d->doc, linkId) & imageFileExtension_GmLinkFlag) { | ||
1197 | iString *imgEntryPath = collect_String(localFilePathFromUrl_String(linkUrl)); | ||
1198 | remove_Block(&imgEntryPath->chars, 0, size_String(localPath) + 1 /* slash, too */); | ||
1199 | setData_Media(media_GmDocument(d->doc), | ||
1200 | linkId, | ||
1201 | collectNewCStr_String(mediaTypeFromPath_String(linkUrl)), | ||
1202 | data_Archive(arch, imgEntryPath), | ||
1203 | 0); | ||
1204 | haveImage = iTrue; | ||
1205 | } | ||
1206 | } | ||
1207 | if (haveImage) { | ||
1208 | redoLayout_GmDocument(d->doc); | ||
1209 | updateVisible_DocumentWidget_(d); | ||
1210 | invalidate_DocumentWidget_(d); | ||
1211 | } | ||
1212 | } | ||
1213 | } | ||
1214 | delete_String(localPath); | ||
1215 | } | ||
1216 | } | ||
1217 | } | ||
1160 | deinit_String(&str); | 1218 | deinit_String(&str); |
1161 | } | 1219 | } |
1162 | } | 1220 | } |