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