| OLD | NEW | 
|---|
| 1 // | 1 // | 
| 2 // FavIcon | 2 // FavIcon | 
| 3 // Copyright © 2016 Leon Breedt | 3 // Copyright © 2016 Leon Breedt | 
| 4 // | 4 // | 
| 5 // Licensed under the Apache License, Version 2.0 (the "License"); | 5 // Licensed under the Apache License, Version 2.0 (the "License"); | 
| 6 // you may not use this file except in compliance with the License. | 6 // you may not use this file except in compliance with the License. | 
| 7 // You may obtain a copy of the License at | 7 // You may obtain a copy of the License at | 
| 8 // | 8 // | 
| 9 // http://www.apache.org/licenses/LICENSE-2.0 | 9 // http://www.apache.org/licenses/LICENSE-2.0 | 
| 10 // | 10 // | 
| 11 // Unless required by applicable law or agreed to in writing, software | 11 // Unless required by applicable law or agreed to in writing, software | 
| 12 // distributed under the License is distributed on an "AS IS" BASIS, | 12 // distributed under the License is distributed on an "AS IS" BASIS, | 
| 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
| 14 // See the License for the specific language governing permissions and | 14 // See the License for the specific language governing permissions and | 
| 15 // limitations under the License. | 15 // limitations under the License. | 
| 16 // | 16 // | 
| 17 | 17 | 
|  | 18 import FavIcon.XMLDocument | 
| 18 import Foundation | 19 import Foundation | 
| 19 import FavIcon.XMLDocument |  | 
| 20 | 20 | 
| 21 /// Represents an icon size. | 21 /// Represents an icon size. | 
| 22 struct IconSize : Hashable, Equatable { | 22 struct IconSize: Hashable, Equatable { | 
| 23   var hashValue: Int { | 23     var hashValue: Int { | 
| 24     return width.hashValue ^ height.hashValue | 24         return width.hashValue ^ height.hashValue | 
| 25   } | 25     } | 
| 26 | 26 | 
| 27   static func ==(lhs: IconSize, rhs: IconSize) -> Bool { | 27     static func == (lhs: IconSize, rhs: IconSize) -> Bool { | 
| 28     return lhs.width == rhs.width && lhs.height == rhs.height | 28         return lhs.width == rhs.width && lhs.height == rhs.height | 
| 29   } | 29     } | 
| 30 | 30 | 
| 31   /// The width of the icon. | 31     /// The width of the icon. | 
| 32   let width: Int | 32     let width: Int | 
| 33   /// The height of the icon. | 33     /// The height of the icon. | 
| 34   let height: Int | 34     let height: Int | 
| 35 } | 35 } | 
| 36 | 36 | 
| 37 private let kRelIconTypeMap: [IconSize: DetectedIconType] = [ | 37 private let kRelIconTypeMap: [IconSize: DetectedIconType] = [ | 
| 38   IconSize(width: 16, height: 16): .classic, | 38     IconSize(width: 16, height: 16): .classic, | 
| 39   IconSize(width: 32, height: 32): .appleOSXSafariTab, | 39     IconSize(width: 32, height: 32): .appleOSXSafariTab, | 
| 40   IconSize(width: 96, height: 96): .googleTV, | 40     IconSize(width: 96, height: 96): .googleTV, | 
| 41   IconSize(width: 192, height: 192): .googleAndroidChrome, | 41     IconSize(width: 192, height: 192): .googleAndroidChrome, | 
| 42   IconSize(width: 196, height: 196): .googleAndroidChrome | 42     IconSize(width: 196, height: 196): .googleAndroidChrome | 
| 43 ] | 43 ] | 
| 44 | 44 | 
| 45 private let kMicrosoftSizeMap: [String: IconSize] = [ | 45 private let kMicrosoftSizeMap: [String: IconSize] = [ | 
| 46   "msapplication-tileimage": IconSize(width: 144, height: 144), | 46     "msapplication-tileimage": IconSize(width: 144, height: 144), | 
| 47   "msapplication-square70x70logo": IconSize(width: 70, height: 70), | 47     "msapplication-square70x70logo": IconSize(width: 70, height: 70), | 
| 48   "msapplication-square150x150logo": IconSize(width: 150, height: 150), | 48     "msapplication-square150x150logo": IconSize(width: 150, height: 150), | 
| 49   "msapplication-wide310x150logo": IconSize(width: 310, height: 150), | 49     "msapplication-wide310x150logo": IconSize(width: 310, height: 150), | 
| 50   "msapplication-square310x310logo": IconSize(width: 310, height: 310), | 50     "msapplication-square310x310logo": IconSize(width: 310, height: 310) | 
| 51 ] | 51 ] | 
| 52 | 52 | 
| 53 private let siteImage: [String: IconSize] = [ | 53 private let siteImage: [String: IconSize] = [ | 
| 54   "og:image": IconSize(width: 1024, height: 512), | 54     "og:image": IconSize(width: 1024, height: 512), | 
| 55   "twitter:image": IconSize(width: 1024, height: 512) | 55     "twitter:image": IconSize(width: 1024, height: 512) | 
| 56 ] | 56 ] | 
| 57 | 57 | 
| 58 /// Extracts a list of icons from the `<head>` section of an HTML document. | 58 /// Extracts a list of icons from the `<head>` section of an HTML document. | 
| 59 /// | 59 /// | 
| 60 /// - parameter document: An HTML document to process. | 60 /// - parameter document: An HTML document to process. | 
| 61 /// - parameter baseURL: A base URL to combine with any relative image paths. | 61 /// - parameter baseURL: A base URL to combine with any relative image paths. | 
| 62 /// - parameter returns: An array of `DetectedIcon` structures. | 62 /// - parameter returns: An array of `DetectedIcon` structures. | 
| 63 // swiftlint:disable function_body_length | 63 // swiftlint:disable function_body_length | 
| 64 // swiftlint:disable cyclomatic_complexity | 64 // swiftlint:disable cyclomatic_complexity | 
| 65 func examineHTMLMeta(_ document: HTMLDocument, baseURL: URL) -> [String:String] 
     { | 65 func examineHTMLMeta(_ document: HTMLDocument, baseURL: URL) -> [String: String]
      { | 
| 66   var resp: [String:String] = [:] | 66     var resp: [String: String] = [:] | 
| 67   for meta in document.query("/html/head/meta") { | 67     for meta in document.query("/html/head/meta") { | 
| 68     if let property = meta.attributes["property"]?.lowercased(), | 68         if let property = meta.attributes["property"]?.lowercased(), | 
| 69       let content = meta.attributes["content"]{ | 69             let content = meta.attributes["content"] { | 
| 70       switch property { | 70             switch property { | 
| 71       case "og:url": | 71             case "og:url": | 
| 72         resp["og:url"] = content; | 72                 resp["og:url"] = content | 
| 73         break | 73             case "og:description": | 
| 74       case "og:description": | 74                 resp["description"] = content | 
| 75         resp["description"] = content; | 75             case "og:image": | 
| 76         break | 76                 resp["image"] = content | 
| 77       case "og:image": | 77             case "og:title": | 
| 78         resp["image"] = content; | 78                 resp["title"] = content | 
| 79         break | 79             case "og:site_name": | 
| 80       case "og:title": | 80                 resp["site_name"] = content | 
| 81         resp["title"] = content; | 81             default: | 
| 82         break | 82                 break | 
| 83       case "og:site_name": | 83             } | 
| 84         resp["site_name"] = content; | 84         } | 
| 85         break | 85         if let name = meta.attributes["name"]?.lowercased(), | 
| 86       default: | 86             let content = meta.attributes["content"], | 
| 87         break | 87             name == "description" { | 
| 88       } | 88             resp["description"] = resp["description"] ?? content | 
|  | 89         } | 
| 89     } | 90     } | 
| 90     if let name = meta.attributes["name"]?.lowercased(), | 91 | 
| 91       let content = meta.attributes["content"], | 92     for title in document.query("/html/head/title") { | 
| 92       name == "description" { | 93         if let titleString = title.contents { | 
| 93       resp["description"] = resp["description"] ?? content; | 94             resp["title"] = resp["title"] ?? titleString | 
|  | 95         } | 
| 94     } | 96     } | 
| 95   } | 97 | 
| 96 | 98     for link in document.query("/html/head/link") { | 
| 97   for title in document.query("/html/head/title") { | 99         if let rel = link.attributes["rel"], | 
| 98     if let titleString = title.contents { | 100             let href = link.attributes["href"], | 
| 99       resp["title"] = resp["title"] ?? titleString; | 101             let url = URL(string: href, relativeTo: baseURL) { | 
|  | 102             switch rel.lowercased() { | 
|  | 103             case "canonical": | 
|  | 104                 resp["canonical"] = url.absoluteString | 
|  | 105             case "amphtml": | 
|  | 106                 resp["amphtml"] = url.absoluteString | 
|  | 107             case "search": | 
|  | 108                 resp["search"] = url.absoluteString | 
|  | 109             case "fluid-icon": | 
|  | 110                 resp["fluid-icon"] = url.absoluteString | 
|  | 111             case "alternate": | 
|  | 112                 let application = link.attributes["application"] | 
|  | 113                 if application == "application/atom+xml" { | 
|  | 114                     resp["atom"] = url.absoluteString | 
|  | 115                 } | 
|  | 116             default: | 
|  | 117                 break | 
|  | 118             } | 
|  | 119         } | 
| 100     } | 120     } | 
| 101   } | 121     return resp | 
| 102 |  | 
| 103   for link in document.query("/html/head/link") { |  | 
| 104     if let rel = link.attributes["rel"], |  | 
| 105       let href = link.attributes["href"], |  | 
| 106       let url = URL(string: href, relativeTo: baseURL) |  | 
| 107     { |  | 
| 108       switch rel.lowercased() { |  | 
| 109       case "canonical": |  | 
| 110         resp["canonical"] = url.absoluteString; |  | 
| 111         break |  | 
| 112       case "amphtml": |  | 
| 113         resp["amphtml"] = url.absoluteString; |  | 
| 114         break |  | 
| 115       case "search": |  | 
| 116         resp["search"] = url.absoluteString; |  | 
| 117         break |  | 
| 118       case "fluid-icon": |  | 
| 119         resp["fluid-icon"] = url.absoluteString; |  | 
| 120         break |  | 
| 121       case "alternate": |  | 
| 122         let application = link.attributes["application"] |  | 
| 123         if application == "application/atom+xml" { |  | 
| 124           resp["atom"] = url.absoluteString; |  | 
| 125         } |  | 
| 126         break |  | 
| 127       default: |  | 
| 128         break |  | 
| 129       } |  | 
| 130     } |  | 
| 131   } |  | 
| 132 |  | 
| 133   return resp; |  | 
| 134 } | 122 } | 
| 135 | 123 | 
| 136 func extractHTMLHeadIcons(_ document: HTMLDocument, baseURL: URL) -> [DetectedIc
     on] { | 124 func extractHTMLHeadIcons(_ document: HTMLDocument, baseURL: URL) -> [DetectedIc
     on] { | 
| 137   var icons: [DetectedIcon] = [] | 125     var icons: [DetectedIcon] = [] | 
| 138 | 126 | 
| 139   for link in document.query("/html/head/link") { | 127     for link in document.query("/html/head/link") { | 
| 140     if let rel = link.attributes["rel"], | 128         if let rel = link.attributes["rel"], | 
| 141       let href = link.attributes["href"], | 129             let href = link.attributes["href"], | 
| 142       let url = URL(string: href, relativeTo: baseURL) { | 130             let url = URL(string: href, relativeTo: baseURL) { | 
| 143       switch rel.lowercased() { | 131             switch rel.lowercased() { | 
| 144       case "shortcut icon": | 132             case "shortcut icon": | 
| 145         icons.append(DetectedIcon(url: url.absoluteURL, type:.shortcut)) | 133                 icons.append(DetectedIcon(url: url.absoluteURL, type: .shortcut)
     ) | 
| 146         break | 134             case "icon": | 
| 147       case "icon": | 135                 if let type = link.attributes["type"], type.lowercased() == "ima
     ge/png" { | 
| 148         if let type = link.attributes["type"], type.lowercased() == "image/png" 
     { | 136                     let sizes = parseHTMLIconSizes(link.attributes["sizes"]) | 
| 149           let sizes = parseHTMLIconSizes(link.attributes["sizes"]) | 137                     if sizes.count > 0 { | 
| 150           if sizes.count > 0 { | 138                         for size in sizes { | 
| 151             for size in sizes { | 139                             if let type = kRelIconTypeMap[size] { | 
| 152               if let type = kRelIconTypeMap[size] { | 140                                 icons.append(DetectedIcon(url: url, | 
|  | 141                                                           type: type, | 
|  | 142                                                           width: size.width, | 
|  | 143                                                           height: size.height)) | 
|  | 144                             } | 
|  | 145                         } | 
|  | 146                     } else { | 
|  | 147                         icons.append(DetectedIcon(url: url.absoluteURL, type: .c
     lassic)) | 
|  | 148                     } | 
|  | 149                 } else { | 
|  | 150                     icons.append(DetectedIcon(url: url.absoluteURL, type: .class
     ic)) | 
|  | 151                 } | 
|  | 152             case "apple-touch-icon": | 
|  | 153                 let sizes = parseHTMLIconSizes(link.attributes["sizes"]) | 
|  | 154                 if sizes.count > 0 { | 
|  | 155                     for size in sizes { | 
|  | 156                         icons.append(DetectedIcon(url: url.absoluteURL, | 
|  | 157                                                   type: .appleIOSWebClip, | 
|  | 158                                                   width: size.width, | 
|  | 159                                                   height: size.height)) | 
|  | 160                     } | 
|  | 161                 } else { | 
|  | 162                     icons.append(DetectedIcon(url: url.absoluteURL, | 
|  | 163                                               type: .appleIOSWebClip, | 
|  | 164                                               width: 60, | 
|  | 165                                               height: 60)) | 
|  | 166                 } | 
|  | 167             default: | 
|  | 168                 break | 
|  | 169             } | 
|  | 170         } | 
|  | 171     } | 
|  | 172 | 
|  | 173     for meta in document.query("/html/head/meta") { | 
|  | 174         if let name = meta.attributes["name"]?.lowercased(), | 
|  | 175             let content = meta.attributes["content"], | 
|  | 176             let url = URL(string: content, relativeTo: baseURL), | 
|  | 177             let size = kMicrosoftSizeMap[name] { | 
|  | 178             icons.append(DetectedIcon(url: url, | 
|  | 179                                       type: .microsoftPinnedSite, | 
|  | 180                                       width: size.width, | 
|  | 181                                       height: size.height)) | 
|  | 182         } else if | 
|  | 183             let property = meta.attributes["property"]?.lowercased(), | 
|  | 184             let content = meta.attributes["content"], | 
|  | 185             let url = URL(string: content, relativeTo: baseURL), | 
|  | 186             let size = siteImage[property] { | 
| 153                 icons.append(DetectedIcon(url: url, | 187                 icons.append(DetectedIcon(url: url, | 
| 154                                           type: type, | 188                                           type: .FBImage, | 
| 155                                           width: size.width, | 189                                           width: size.width, | 
| 156                                           height: size.height)) | 190                                           height: size.height)) | 
| 157               } |  | 
| 158             } |  | 
| 159           } else { |  | 
| 160             icons.append(DetectedIcon(url: url.absoluteURL, type: .classic)) |  | 
| 161           } |  | 
| 162         } else { |  | 
| 163           icons.append(DetectedIcon(url: url.absoluteURL, type: .classic)) |  | 
| 164         } | 191         } | 
| 165       case "apple-touch-icon": |  | 
| 166         let sizes = parseHTMLIconSizes(link.attributes["sizes"]) |  | 
| 167         if sizes.count > 0 { |  | 
| 168           for size in sizes { |  | 
| 169             icons.append(DetectedIcon(url: url.absoluteURL, |  | 
| 170                                       type: .appleIOSWebClip, |  | 
| 171                                       width: size.width, |  | 
| 172                                       height: size.height)) |  | 
| 173           } |  | 
| 174         } else { |  | 
| 175           icons.append(DetectedIcon(url: url.absoluteURL, |  | 
| 176                                     type: .appleIOSWebClip, |  | 
| 177                                     width: 60, |  | 
| 178                                     height: 60)) |  | 
| 179         } |  | 
| 180       default: |  | 
| 181         break |  | 
| 182       } |  | 
| 183     } | 192     } | 
| 184   } | 193 | 
| 185 | 194     return icons | 
| 186   for meta in document.query("/html/head/meta") { |  | 
| 187     if let name = meta.attributes["name"]?.lowercased(), |  | 
| 188       let content = meta.attributes["content"], |  | 
| 189       let url = URL(string: content, relativeTo: baseURL), |  | 
| 190       let size = kMicrosoftSizeMap[name] { |  | 
| 191       icons.append(DetectedIcon(url: url, |  | 
| 192                                 type: .microsoftPinnedSite, |  | 
| 193                                 width: size.width, |  | 
| 194                                 height: size.height)) |  | 
| 195     } else if |  | 
| 196       let property = meta.attributes["property"]?.lowercased(), |  | 
| 197       let content = meta.attributes["content"], |  | 
| 198       let url = URL(string: content, relativeTo: baseURL), |  | 
| 199       let size = siteImage[property] { |  | 
| 200       icons.append(DetectedIcon(url: url, |  | 
| 201                                 type: .FBImage, |  | 
| 202                                 width: size.width, |  | 
| 203                                 height: size.height)) |  | 
| 204     } |  | 
| 205   } |  | 
| 206 |  | 
| 207   return icons |  | 
| 208 } | 195 } | 
| 209 // swiftlint:enable cyclomatic_complexity | 196 // swiftlint:enable cyclomatic_complexity | 
| 210 // swiftlint:enable function_body_length | 197 // swiftlint:enable function_body_length | 
| 211 | 198 | 
| 212 /// Extracts a list of icons from a Web Application Manifest file | 199 /// Extracts a list of icons from a Web Application Manifest file | 
| 213 /// | 200 /// | 
| 214 /// - parameter jsonString: A JSON string containing the contents of the manifes
     t file. | 201 /// - parameter jsonString: A JSON string containing the contents of the manifes
     t file. | 
| 215 /// - parameter baseURL: A base URL to combine with any relative image paths. | 202 /// - parameter baseURL: A base URL to combine with any relative image paths. | 
| 216 /// - returns: An array of `DetectedIcon` structures. | 203 /// - returns: An array of `DetectedIcon` structures. | 
| 217 func extractManifestJSONIcons(_ jsonString: String, baseURL: URL) -> [DetectedIc
     on] { | 204 func extractManifestJSONIcons(_ jsonString: String, baseURL: URL) -> [DetectedIc
     on] { | 
| 218   var icons: [DetectedIcon] = [] | 205     var icons: [DetectedIcon] = [] | 
| 219 | 206 | 
| 220   if let data = jsonString.data(using: String.Encoding.utf8), | 207     if let data = jsonString.data(using: String.Encoding.utf8), | 
| 221     let object = try? JSONSerialization.jsonObject(with: data, options: JSONSeri
     alization.ReadingOptions()), | 208         let object = try? JSONSerialization.jsonObject(with: data, options: JSON
     Serialization.ReadingOptions()), | 
| 222     let manifest = object as? NSDictionary, | 209         let manifest = object as? NSDictionary, | 
| 223     let manifestIcons = manifest["icons"] as? [NSDictionary] { | 210         let manifestIcons = manifest["icons"] as? [NSDictionary] { | 
| 224     for icon in manifestIcons { | 211         for icon in manifestIcons { | 
| 225       if let type = icon["type"] as? String, type.lowercased() == "image/png", | 212             if let type = icon["type"] as? String, type.lowercased() == "image/p
     ng", | 
| 226         let src = icon["src"] as? String, | 213                 let src = icon["src"] as? String, | 
| 227         let url = URL(string: src, relativeTo: baseURL)?.absoluteURL { | 214                 let url = URL(string: src, relativeTo: baseURL)?.absoluteURL { | 
| 228         let sizes = parseHTMLIconSizes(icon["sizes"] as? String) | 215                 let sizes = parseHTMLIconSizes(icon["sizes"] as? String) | 
| 229         if sizes.count > 0 { | 216                 if sizes.count > 0 { | 
| 230           for size in sizes { | 217                     for size in sizes { | 
| 231             icons.append(DetectedIcon(url: url, | 218                         icons.append(DetectedIcon(url: url, | 
| 232                                       type: .webAppManifest, | 219                                                   type: .webAppManifest, | 
| 233                                       width: size.width, | 220                                                   width: size.width, | 
| 234                                       height: size.height)) | 221                                                   height: size.height)) | 
| 235           } | 222                     } | 
| 236         } else { | 223                 } else { | 
| 237           icons.append(DetectedIcon(url: url, type: .webAppManifest)) | 224                     icons.append(DetectedIcon(url: url, type: .webAppManifest)) | 
|  | 225                 } | 
|  | 226             } | 
| 238         } | 227         } | 
| 239       } |  | 
| 240     } | 228     } | 
| 241   } | 229 | 
| 242 | 230     return icons | 
| 243   return icons |  | 
| 244 } | 231 } | 
| 245 | 232 | 
| 246 /// Extracts a list of icons from a Microsoft browser configuration XML document
     . | 233 /// Extracts a list of icons from a Microsoft browser configuration XML document
     . | 
| 247 /// | 234 /// | 
| 248 /// - parameter document: An `XMLDocument` for the Microsoft browser configurati
     on file. | 235 /// - parameter document: An `XMLDocument` for the Microsoft browser configurati
     on file. | 
| 249 /// - parameter baseURL: A base URL to combine with any relative image paths. | 236 /// - parameter baseURL: A base URL to combine with any relative image paths. | 
| 250 /// - returns: An array of `DetectedIcon` structures. | 237 /// - returns: An array of `DetectedIcon` structures. | 
| 251 func extractBrowserConfigXMLIcons(_ document: LBXMLDocument, baseURL: URL) -> [D
     etectedIcon] { | 238 func extractBrowserConfigXMLIcons(_ document: LBXMLDocument, baseURL: URL) -> [D
     etectedIcon] { | 
| 252   var icons: [DetectedIcon] = [] | 239     var icons: [DetectedIcon] = [] | 
| 253 | 240 | 
| 254   for tile in document.query("/browserconfig/msapplication/tile/*") { | 241     for tile in document.query("/browserconfig/msapplication/tile/*") { | 
| 255     if let src = tile.attributes["src"], | 242         if let src = tile.attributes["src"], | 
| 256       let url = URL(string: src, relativeTo: baseURL)?.absoluteURL { | 243             let url = URL(string: src, relativeTo: baseURL)?.absoluteURL { | 
| 257       switch tile.name.lowercased() { | 244             switch tile.name.lowercased() { | 
| 258       case "tileimage": | 245             case "tileimage": | 
| 259         icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 1
     44, height: 144)) | 246                 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, 
     width: 144, height: 144)) | 
| 260         break | 247             case "square70x70logo": | 
| 261       case "square70x70logo": | 248                 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, 
     width: 70, height: 70)) | 
| 262         icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 7
     0, height: 70)) | 249             case "square150x150logo": | 
| 263         break | 250                 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, 
     width: 150, height: 150)) | 
| 264       case "square150x150logo": | 251             case "wide310x150logo": | 
| 265         icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 1
     50, height: 150)) | 252                 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, 
     width: 310, height: 150)) | 
| 266         break | 253             case "square310x310logo": | 
| 267       case "wide310x150logo": | 254                 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, 
     width: 310, height: 310)) | 
| 268         icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 3
     10, height: 150)) | 255             default: | 
| 269         break | 256                 break | 
| 270       case "square310x310logo": | 257             } | 
| 271         icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 3
     10, height: 310)) | 258         } | 
| 272         break |  | 
| 273       default: |  | 
| 274         break |  | 
| 275       } |  | 
| 276     } | 259     } | 
| 277   } | 260 | 
| 278 | 261     return icons | 
| 279   return icons |  | 
| 280 } | 262 } | 
| 281 | 263 | 
| 282 /// Extracts the Web App Manifest URLs from an HTML document, if any. | 264 /// Extracts the Web App Manifest URLs from an HTML document, if any. | 
| 283 /// | 265 /// | 
| 284 /// - parameter document: The HTML document to scan for Web App Manifest URLs | 266 /// - parameter document: The HTML document to scan for Web App Manifest URLs | 
| 285 /// - parameter baseURL: The base URL that any 'href' attributes are relative to
     . | 267 /// - parameter baseURL: The base URL that any 'href' attributes are relative to
     . | 
| 286 /// - returns: An array of Web App Manifest `URL`s. | 268 /// - returns: An array of Web App Manifest `URL`s. | 
| 287 func extractWebAppManifestURLs(_ document: HTMLDocument, baseURL: URL) -> [URL] 
     { | 269 func extractWebAppManifestURLs(_ document: HTMLDocument, baseURL: URL) -> [URL] 
     { | 
| 288   var urls: [URL] = [] | 270     var urls: [URL] = [] | 
| 289   for link in document.query("/html/head/link") { | 271     for link in document.query("/html/head/link") { | 
| 290     if let rel = link.attributes["rel"]?.lowercased(), rel == "manifest", | 272         if let rel = link.attributes["rel"]?.lowercased(), rel == "manifest", | 
| 291       let href = link.attributes["href"], let manifestURL = URL(string: href, re
     lativeTo: baseURL) { | 273             let href = link.attributes["href"], let manifestURL = URL(string: hr
     ef, relativeTo: baseURL) { | 
| 292       urls.append(manifestURL) | 274             urls.append(manifestURL) | 
|  | 275         } | 
| 293     } | 276     } | 
| 294   } | 277     return urls | 
| 295   return urls |  | 
| 296 } | 278 } | 
| 297 | 279 | 
| 298 /// Extracts the first browser config XML file URL from an HTML document, if any
     . | 280 /// Extracts the first browser config XML file URL from an HTML document, if any
     . | 
| 299 /// | 281 /// | 
| 300 /// - parameter document: The HTML document to scan for browser config XML file 
     URLs. | 282 /// - parameter document: The HTML document to scan for browser config XML file 
     URLs. | 
| 301 /// - parameter baseURL: The base URL that any 'href' attributes are relative to
     . | 283 /// - parameter baseURL: The base URL that any 'href' attributes are relative to
     . | 
| 302 /// - returns: A named tuple describing the file URL or a flag indicating that t
     he server | 284 /// - returns: A named tuple describing the file URL or a flag indicating that t
     he server | 
| 303 ///            explicitly requested that the file not be downloaded. | 285 ///            explicitly requested that the file not be downloaded. | 
| 304 func extractBrowserConfigURL(_ document: HTMLDocument, baseURL: URL) -> (url: UR
     L?, disabled: Bool) { | 286 func extractBrowserConfigURL(_ document: HTMLDocument, baseURL: URL) -> (url: UR
     L?, disabled: Bool) { | 
| 305   for meta in document.query("/html/head/meta") { | 287     for meta in document.query("/html/head/meta") { | 
| 306     if let name = meta.attributes["name"]?.lowercased(), name == "msapplication-
     config", | 288         if let name = meta.attributes["name"]?.lowercased(), name == "msapplicat
     ion-config", | 
| 307       let content = meta.attributes["content"] { | 289             let content = meta.attributes["content"] { | 
| 308       if content.lowercased() == "none" { | 290             if content.lowercased() == "none" { | 
| 309         // Explicitly asked us not to download the file. | 291                 // Explicitly asked us not to download the file. | 
| 310         return (url: nil, disabled: true) | 292                 return (url: nil, disabled: true) | 
| 311       } else { | 293             } else { | 
| 312         return (url: URL(string: content, relativeTo: baseURL)?.absoluteURL, dis
     abled: false) | 294                 return (url: URL(string: content, relativeTo: baseURL)?.absolute
     URL, disabled: false) | 
| 313       } | 295             } | 
|  | 296         } | 
| 314     } | 297     } | 
| 315   } | 298     return (url: nil, disabled: false) | 
| 316   return (url: nil, disabled: false) |  | 
| 317 } | 299 } | 
| 318 | 300 | 
| 319 /// Helper function for parsing a W3 `sizes` attribute value. | 301 /// Helper function for parsing a W3 `sizes` attribute value. | 
| 320 /// | 302 /// | 
| 321 /// - parameter string: If not `nil`, the value of the attribute to parse (e.g. 
     `50x50 144x144`). | 303 /// - parameter string: If not `nil`, the value of the attribute to parse (e.g. 
     `50x50 144x144`). | 
| 322 /// - returns: An array of `IconSize` structs for each size found. | 304 /// - returns: An array of `IconSize` structs for each size found. | 
| 323 func parseHTMLIconSizes(_ string: String?) -> [IconSize] { | 305 func parseHTMLIconSizes(_ string: String?) -> [IconSize] { | 
| 324   var sizes: [IconSize] = [] | 306     var sizes: [IconSize] = [] | 
| 325   if let string = string?.lowercased(), string != "any" { | 307     if let string = string?.lowercased(), string != "any" { | 
| 326     for size in string.components(separatedBy: .whitespaces) { | 308         for size in string.components(separatedBy: .whitespaces) { | 
| 327       let parts = size.components(separatedBy: "x") | 309             let parts = size.components(separatedBy: "x") | 
| 328       if parts.count != 2 { continue } | 310             if parts.count != 2 { continue } | 
| 329       if let width = Int(parts[0]), let height = Int(parts[1]) { | 311             if let width = Int(parts[0]), let height = Int(parts[1]) { | 
| 330         sizes.append(IconSize(width: width, height: height)) | 312                 sizes.append(IconSize(width: width, height: height)) | 
| 331       } | 313             } | 
|  | 314         } | 
| 332     } | 315     } | 
| 333   } | 316     return sizes | 
| 334   return sizes |  | 
| 335 } | 317 } | 
| 336 |  | 
| OLD | NEW | 
|---|