diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-05-02 09:07:15 +0300 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-05-02 09:07:15 +0300 |
commit | a14895149fad1724e0f4b4df1fd5834bddc4ead4 (patch) | |
tree | 15795172fe0f47c6ed06a707a520d506d768e564 | |
parent | eae0ef64d071e2702fffc1d00f223124f8c6d8b4 (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.txt | 2 | ||||
-rw-r--r-- | src/gempub.c | 210 | ||||
-rw-r--r-- | src/gempub.h | 58 | ||||
-rw-r--r-- | src/gmrequest.c | 21 | ||||
-rw-r--r-- | src/gmutil.c | 17 | ||||
-rw-r--r-- | src/gmutil.h | 5 | ||||
-rw-r--r-- | src/mimehooks.c | 112 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 145 |
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 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. 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 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY 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 | ||
21 | SOFTWARE, 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 | |||
33 | const char *mimeType_Gempub = "application/gpub+zip"; | ||
34 | |||
35 | struct Impl_Gempub { | ||
36 | iArchive *arch; | ||
37 | iString baseUrl; | ||
38 | iString props[max_GempubProperty]; | ||
39 | }; | ||
40 | |||
41 | iDefineTypeConstruction(Gempub) | ||
42 | |||
43 | void 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 | |||
51 | void 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 | |||
59 | static 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 | |||
101 | iBool 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 | |||
111 | iBool 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 | |||
124 | iBool 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 | |||
133 | void 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 | |||
142 | iBool isOpen_Gempub(const iGempub *d) { | ||
143 | return d->arch != NULL; | ||
144 | } | ||
145 | |||
146 | void setBaseUrl_Gempub(iGempub *d, const iString *url) { | ||
147 | set_String(&d->baseUrl, url); | ||
148 | } | ||
149 | |||
150 | static iBool hasProperty_Gempub_(const iGempub *d, enum iGempubProperty prop) { | ||
151 | return !isEmpty_String(&d->props[prop]); | ||
152 | } | ||
153 | |||
154 | static 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 | |||
161 | static iBool isRemote_Gempub_(const iGempub *d) { | ||
162 | return !equalCase_Rangecc(urlScheme_String(&d->baseUrl), "file"); | ||
163 | } | ||
164 | |||
165 | iBlock *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 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. 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 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY 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 | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #pragma once | ||
24 | |||
25 | #include <the_Foundation/string.h> | ||
26 | |||
27 | iDeclareType(Gempub) | ||
28 | iDeclareTypeConstruction(Gempub) | ||
29 | |||
30 | enum 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 | |||
46 | iBool open_Gempub (iGempub *, const iBlock *data); | ||
47 | iBool openFile_Gempub (iGempub *, const iString *path); | ||
48 | iBool openUrl_Gempub (iGempub *, const iString *url); | ||
49 | void close_Gempub (iGempub *); | ||
50 | |||
51 | void setBaseUrl_Gempub (iGempub *, const iString *baseUrl); | ||
52 | |||
53 | iBool isOpen_Gempub (const iGempub *); | ||
54 | iBlock * coverPageSource_Gempub (const iGempub *); | ||
55 | |||
56 | const iString *property_Gempub (const iGempub *, enum iGempubProperty); | ||
57 | |||
58 | extern 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 | ||
538 | static 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 | |||
553 | static iBool isDirectory_(const iString *path) { | 538 | static 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 | ||
441 | const char *mediaTypeFromPath_String(const iString *path) { | 441 | const 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 | |||
456 | const 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 *); | |||
119 | void urlEncodeSpaces_String (iString *); | 119 | void urlEncodeSpaces_String (iString *); |
120 | const iString * withSpacesEncoded_String(const iString *); | 120 | const iString * withSpacesEncoded_String(const iString *); |
121 | 121 | ||
122 | const char * mediaTypeFromPath_String (const iString *path); | 122 | const char * mediaType_Path (const iString *path); |
123 | iRangecc mediaTypeWithoutParameters_Rangecc (iRangecc mime); | 123 | iRangecc mediaTypeWithoutParameters_Rangecc (iRangecc mime); |
124 | 124 | ||
125 | const iString * findContainerArchive_Path (const iString *path); | ||
126 | |||
127 | |||
125 | const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */ | 128 | const 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 | ||
206 | static void appendGemPubProperty_(iString *out, const char *key, const iString *value) { | 206 | iBlock *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 | |||
212 | iBlock *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 | } |
304 | cleanup: | 214 | delete_Gempub(gempub); |
305 | iRelease(arch); | ||
306 | return output; | 215 | return output; |
307 | } | 216 | } |
308 | 217 | ||
309 | /*----------------------------------------------------------------------------------------------*/ | 218 | /*----------------------------------------------------------------------------------------------*/ |
310 | 219 | ||
311 | static const char *gpubMimeType_MimeHooks_ = "application/gpub+zip"; | ||
312 | static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt"; | 220 | static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt"; |
313 | 221 | ||
314 | struct Impl_MimeHooks { | 222 | struct Impl_MimeHooks { |
@@ -331,7 +239,7 @@ void deinit_MimeHooks(iMimeHooks *d) { | |||
331 | static iBool checkGemPub_(const iString *mime, const iString *requestUrl) { | 239 | static 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 | ||
337 | iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) { | 245 | iBool 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 | ||
1052 | static const char *zipPageHeading_(const iRangecc mime) { | 1053 | static 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 | ||
1069 | static 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 | |||
1068 | static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response, | 1119 | static 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 | ||
1293 | static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { | 1312 | static 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; |