├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── SwiftyOpenGraph │ ├── Internal │ ├── _KeyValuePair.swift │ ├── _getDate.swift │ └── _getDuration.swift │ ├── OpenGraph.Determiner.swift │ ├── OpenGraph.swift │ ├── OpenGraphAudio.swift │ ├── OpenGraphImage.swift │ ├── OpenGraphType.swift │ ├── OpenGraphType │ ├── ArticleAttributes.swift │ ├── BookAttributes.swift │ ├── Music │ │ ├── AlbumAttributes.swift │ │ ├── PlaylistAttributes.swift │ │ ├── RadioStationAttributes.swift │ │ └── SongAttributes.swift │ ├── ProfileAttributes.swift │ ├── Video │ │ ├── Actor.swift │ │ └── SubKind.swift │ └── VideoAttributes.swift │ └── OpenGraphVideo.swift └── Tests └── SwiftyOpenGraphTests ├── Examples ├── audio-array.html ├── audio-url.html ├── image-array.html ├── image-url.html ├── min.html ├── optional.html ├── required.html └── video-movie.html └── OpenGraphTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: macos-13 12 | strategy: 13 | matrix: 14 | swift: ["5.5"] 15 | steps: 16 | - uses: maxim-lobanov/setup-xcode@v1 17 | with: 18 | xcode-version: 15 19 | - uses: actions/checkout@v3 20 | - name: Build 21 | run: swift build -v 22 | - name: Run tests 23 | run: swift test -v 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode user settings 5 | xcuserdata/ -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SchafKit", 6 | "repositoryURL": "https://github.com/FiveSheepCo/SchafKit.git", 7 | "state": { 8 | "branch": "master", 9 | "revision": "8567f948567e4d6fccb80cca1402e1044a4b01e0", 10 | "version": null 11 | } 12 | }, 13 | { 14 | "package": "SwiftSoup", 15 | "repositoryURL": "https://github.com/scinfu/SwiftSoup", 16 | "state": { 17 | "branch": "master", 18 | "revision": "3fa09f4d79e5172b14cb50e02f1d5f115a2bbaef", 19 | "version": null 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftyOpenGraph", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .iOS(.v13), 11 | .watchOS(.v6), 12 | .tvOS(.v13) 13 | ], 14 | products: [ 15 | // Products define the executables and libraries a package produces, and make them visible to other packages. 16 | .library( 17 | name: "SwiftyOpenGraph", 18 | targets: ["SwiftyOpenGraph"] 19 | ), 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/FiveSheepCo/SchafKit.git", .branch("master")), 23 | .package(url: "https://github.com/scinfu/SwiftSoup", .branch("master")), 24 | ], 25 | targets: [ 26 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 27 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 28 | .target( 29 | name: "SwiftyOpenGraph", 30 | dependencies: ["SwiftSoup", "SchafKit"] 31 | ), 32 | .testTarget( 33 | name: "SwiftyOpenGraphTests", 34 | dependencies: ["SwiftyOpenGraph"], 35 | resources: [.copy("Examples")] 36 | ), 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftyOpenGraph 2 | 3 | [![GithubCI_Status]][GithubCI_URL] [![LICENSE_BADGE]][LICENSE_URL] ![Platform](https://img.shields.io/badge/platforms-iOS%2013.0%20%7C%20macOS%2010.15%20%7C%20tvOS%2013.0%20%7C%20watchOS%206.0-F28D00.svg) 4 | 5 | - [Usage](#usage) 6 | - [Initialization](#initialization) 7 | - [Base Properties](#base-properties) 8 | - [Types](#types) 9 | - [Installation](#installation) 10 | - [License](#license) 11 | 12 | # Usage 13 | 14 | ## Initialization 15 | 16 | You use SwiftyOpenGraph by initializing `OpenGraph`, the root object of SwiftyOpenGraph. There are two initializers of `OpenGraph`: 17 | 18 | ### HTML String 19 | ```swift 20 | let graph = OpenGraph(html: htmlString) 21 | ``` 22 | 23 | ### URL 24 | ```swift 25 | let graph = try await OpenGraph(url: "https://quintschaf.com/#/app/mykeyboard") 26 | ``` 27 | 28 | Both of these initializers are optional, only returning when the html contains at least the required OpenGraph properties. 29 | 30 | ## Base Properties 31 | 32 | Every valid open graph enabled webpage has four properties, which an `OpenGraph` object exposes as non-optional constants: `title`, `type`, `image` and `url`. It can also contain `additionalImages`, `audios`, `videos`, `description`, `determiner`, `locale`, `alternateLocales` and `siteName`. These are either optional constants of an `OpenGraph` object or they contain the default value as defined by the OpenGraph protocol. 33 | 34 | Images, Audios, Videos and the Determiner are represented by `OpenGraphImage`, `OpenGraphAudio`, `OpenGraphVideo` and `Determiner`. All of them are structs that may contain additional data, e.g. the width of an image, represented by the `"og:image:width"` meta tag. 35 | 36 | Example: 37 | ```swift 38 | print(graph.title) // "MyKeyboard" 39 | print(graph.type) // OpenGraphType.website 40 | print(graph.image) // OpenGraphImage(url: "...", width: ...) 41 | print(graph.url) // "https://quintschaf.com/#/app/mykeyboard" 42 | 43 | print(graph.additionalImages) // [] 44 | print(graph.audios) // [] 45 | print(graph.videos) // [] 46 | print(graph.description) // Optional("The fully customizable Keyboard.") 47 | print(graph.determiner) // Determiner.blank 48 | print(graph.locale) // "en_US" 49 | print(graph.alternateLocales) // [] 50 | print(graph.siteName) // nil 51 | ``` 52 | 53 | ## Types 54 | 55 | The type (`"og:type"`) is represented by the `OpenGraphType` enum, which has the following cases (as specified by the OpenGraph spec): 56 | - `song(SongAttributes)` 57 | - `album(AlbumAttributes)` 58 | - `playlist(PlaylistAttributes)` 59 | - `radioStation(RadioStationAttributes)` 60 | - `video(VideoAttributes)` 61 | - `article(ArticleAttributes)` 62 | - `book(BookAttributes)` 63 | - `profile(ProfileAttributes)` 64 | - `website` 65 | 66 | All of these (with the exception of the default type `website`) have a struct as an associated value. These structs hold the values that are specific to the type. Due to their differences in properties, there are seperate cases for `song`, `album`, `playlist` and `radioStation`. Due to them having almost the same properties, there is only one `video` case for all video types. The `VideoAttributes` therefor contains a `kind` property that is an enumeration containing all the video types. 67 | 68 | Example: 69 | ```swift 70 | switch graph.type { 71 | case .video(let attributes): 72 | print(attributes.kind) // VideoAttributes.SubKind.movie 73 | print(attributes.actors) // [] 74 | print(attributes.directors) // [] 75 | print(attributes.writers) // [] 76 | print(attributes.duration) // Optional(200) 77 | print(attributes.releaseDate) // Optional(Date(...)) 78 | print(attributes.tags) // ["some", "words", "as", "tags"] 79 | print(attributes.series) // nil 80 | default: 81 | break 82 | } 83 | ``` 84 | 85 | # Installation 86 | 87 | ### Swift Package Manager 88 | 89 | SwiftyOpenGraph relies on Swift Package Manager and is installed by adding it as a dependency. 90 | 91 | # License 92 | 93 | We have chosen to use the CC0 1.0 Universal license for SwiftyOpenGraph. The following short explanation has no legal implication whatsoever and does not override the license in any way: CC0 1.0 Universal license gives you the right to use or modify all of SwiftyOpenGraphs code in any (commercial or non-commercial) product without mentioning, licensing or other headaches of any kind. 94 | 95 | # Background 96 | 97 | When we were trying to find an OpenGraph implementation in Swift there was only one result. That result was using Regular Expressions to parse the meta tags, which we find unacceptable. So we set out to create one that uses `SwiftSoup` to properly parse the html of a webpage. We also wanted to make sure this project is a perfect 1:1 abstraction of the OpenGraph protocol into Swift. If there are any additions or changes made to the protocol, we will adopt them as fast as possible. 98 | 99 | 100 | 101 | [GithubCI_Status]: https://github.com/Quintschaf/SwiftyOpenGraph/actions/workflows/swift.yml/badge.svg?branch=main 102 | [GithubCI_URL]: https://github.com/Quintschaf/SwiftyOpenGraph/actions/workflows/swift.yml 103 | [LICENSE_BADGE]: https://badgen.net/github/license/quintschaf/SwiftyOpenGraph 104 | [LICENSE_URL]: https://github.com/Quintschaf/SwiftyOpenGraph/blob/master/LICENSE 105 | -------------------------------------------------------------------------------- /Sources/SwiftyOpenGraph/Internal/_KeyValuePair.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct _KeyValuePair { 4 | let key: String 5 | let value: String 6 | } 7 | -------------------------------------------------------------------------------- /Sources/SwiftyOpenGraph/Internal/_getDate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func _getDate(from string: String?) -> Date? { 4 | string.flatMap { (dateString: String) in 5 | let optionSets: [ISO8601DateFormatter.Options] = [ 6 | [.withFullDate, .withDashSeparatorInDate], 7 | [.withInternetDateTime], 8 | [.withFullDate, .withFullTime, .withSpaceBetweenDateAndTime], 9 | [.withYear, .withWeekOfYear, .withDashSeparatorInDate], 10 | [.withYear, .withWeekOfYear, .withDay, .withDashSeparatorInDate], 11 | [.withYear, .withDay, .withDashSeparatorInDate] 12 | ] 13 | 14 | for options in optionSets { 15 | let formatter = ISO8601DateFormatter() 16 | formatter.formatOptions = options 17 | if let date = formatter.date(from: dateString) { 18 | return date 19 | } 20 | } 21 | 22 | return nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftyOpenGraph/Internal/_getDuration.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func _getDuration(from string: String?) -> Int? { 4 | string.flatMap { (durationString: String) in 5 | if let duration = Int(durationString) { 6 | return duration 7 | } else if let match = durationString.regexMatches(with: "PT(\\d+)M(\\d+)S")?.first, 8 | let minutes = match.captureGroups[0]?.toInt, 9 | let seconds = match.captureGroups[1]?.toInt { 10 | return minutes * 60 + seconds 11 | } else { 12 | return nil 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftyOpenGraph/OpenGraph.Determiner.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension OpenGraph { 4 | 5 | /// An enum of (`a`, `an`, `the`, "", `auto`). If `auto` is chosen, the consumer of your data should chose between `a` or `an`. 6 | public enum Determiner: String { 7 | case a, an, the, blank = "", auto 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/SwiftyOpenGraph/OpenGraph.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftSoup 3 | import SchafKit 4 | 5 | public struct OpenGraph { 6 | /// The title of your object as it should appear within the graph, e.g., "The Rock". 7 | public let title: String 8 | /// The type of your object, e.g., "video.movie". Depending on the type you specify, other properties may also be required. 9 | public let type: OpenGraphType 10 | /// An image URL which should represent your object within the graph. 11 | public let image: OpenGraphImage 12 | /// The canonical URL of your object that will be used as its permanent ID in the graph, e.g., "https://www.imdb.com/title/tt0117500/". 13 | public let url: String 14 | 15 | /// Other images included. 16 | public let additionalImages: [OpenGraphImage] 17 | 18 | /// Audio files to accompany this object. 19 | public let audios: [OpenGraphAudio] 20 | /// Video files that complement this object. 21 | public let videos: [OpenGraphVideo] 22 | /// A one to two sentence description of your object. 23 | public let description: String? 24 | /// The word that appears before this object's title in a sentence. Default is "" (blank). 25 | public let determiner : Determiner 26 | /// The locale these tags are marked up in. Of the format `language_TERRITORY`. Default is `en_US`. 27 | public let locale: String 28 | /// An array of other locales this page is available in. 29 | public let alternateLocales: [String] 30 | /// If your object is part of a larger web site, the name which should be displayed for the overall site. e.g., "IMDb". 31 | public let siteName: String? 32 | 33 | internal enum Constants { 34 | static let metaTag = "meta" 35 | 36 | static let propertyAttribute = "property" 37 | static let contentAttribute = "content" 38 | 39 | static let titleProperty = "og:title" 40 | static let urlProperty = "og:url" 41 | 42 | // TODO: Implement these 43 | // Optional properties 44 | static let audioProperty = "og:audio" 45 | static let descriptionProperty = "og:description" 46 | static let determinerProperty = "og:determiner" 47 | static let localeProperty = "og:locale" 48 | static let alternateLocaleProperty = "og:locale:alternate" 49 | static let siteNameProperty = "og:site_name" 50 | static let videoProperty = "og:video" 51 | 52 | // Default values 53 | static let defaultLocale = "en_US" 54 | static let defaultDeterminer = Determiner.blank 55 | } 56 | 57 | public init?(url: String) async throws { 58 | guard 59 | let html = 60 | try await SKNetworking 61 | .request( 62 | url: url, 63 | options: [ 64 | .headerFields(value: [.userAgent: "Googlebot"]) // Some websites require this to return the open graph values 65 | ] 66 | ) 67 | .stringValue else { 68 | return nil 69 | } 70 | 71 | self.init(html: html) 72 | } 73 | 74 | public init?(html: String) { 75 | do { 76 | let doc: Document = try SwiftSoup.parse(html) 77 | 78 | // Put all meta properties into a key-value pair array 79 | let parsed = try doc.select(Constants.metaTag).map({ element in 80 | _KeyValuePair( 81 | key: try element.attr(Constants.propertyAttribute), 82 | value: try element.attr(Constants.contentAttribute) 83 | ) 84 | }) 85 | 86 | func getFirstValue(for key: String) -> String? { 87 | parsed.first(where: { $0.key == key })?.value 88 | } 89 | 90 | // Find required single values title and url 91 | guard 92 | let title = getFirstValue(for: Constants.titleProperty), 93 | let url = getFirstValue(for: Constants.urlProperty) else { 94 | return nil 95 | } 96 | 97 | // Find images 98 | var images: [OpenGraphImage] = [] 99 | var audios: [OpenGraphAudio] = [] 100 | var videos: [OpenGraphVideo] = [] 101 | for (index, kVP) in parsed.enumerated() { 102 | 103 | func getRemainingKVPs() -> [_KeyValuePair] { 104 | Array(parsed[index+1.. 2 | 3 | 4 | 5 | Two structured audio properties 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |

Audio property with type declaration as structured property.

27 | 28 | 29 | -------------------------------------------------------------------------------- /Tests/SwiftyOpenGraphTests/Examples/audio-url.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Structured audio property 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

Audio defined as structured properties.

23 |

Compare to og:audio alias for parser support of og:audio:url vs. og:audio.

24 | 25 | -------------------------------------------------------------------------------- /Tests/SwiftyOpenGraphTests/Examples/image-array.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Two structured image properties 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |

Two images declared as an array.

26 | 27 | 28 | -------------------------------------------------------------------------------- /Tests/SwiftyOpenGraphTests/Examples/image-url.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Full structured image property 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

Example of basic properties with a structured image including og:image:url.

20 |

Compare to og:image alias for parser support of og:image:url vs. og:image.

21 | 22 | -------------------------------------------------------------------------------- /Tests/SwiftyOpenGraphTests/Examples/min.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Minimal HTML 6 | 7 | 8 | 9 | 10 | logo 11 |

An example of an HTML document lacking required properties.

12 |

Contains optional properties and HTML fallback content.

13 | 14 | -------------------------------------------------------------------------------- /Tests/SwiftyOpenGraphTests/Examples/optional.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Open Graph protocol examples 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Tests/SwiftyOpenGraphTests/Examples/required.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Minimum required properties 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Tests/SwiftyOpenGraphTests/Examples/video-movie.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Arrival of a Train at La Ciotat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |

A video.movie object.

41 | 42 | -------------------------------------------------------------------------------- /Tests/SwiftyOpenGraphTests/OpenGraphTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SchafKit 3 | @testable import SwiftyOpenGraph 4 | 5 | final class OpenGraphTests: XCTestCase { 6 | 7 | func getHtml(for url: String) async -> String { 8 | let result = try! await SKNetworking.request(url: url) 9 | 10 | return .init(data: result.data, encoding: .utf8)! 11 | } 12 | 13 | func test(date: Date?, shortUSString: String) { 14 | XCTAssertNotNil(date) 15 | 16 | let formatter = DateFormatter(dateStyle: .short) 17 | formatter.locale = .init(identifier: "en_US") 18 | XCTAssertEqual(formatter.string(from: date!), shortUSString) 19 | } 20 | 21 | func getGraph(filename: String) -> OpenGraph? { 22 | do { 23 | let thisSourceFile = URL(fileURLWithPath: #file) 24 | let thisDirectory = thisSourceFile.deletingLastPathComponent().absoluteString 25 | let html = try String(contentsOf: URL(string: "\(thisDirectory)Examples/\(filename).html")!) 26 | 27 | return OpenGraph(html: html) 28 | } 29 | catch let err { 30 | XCTFail("File retrieval failed with error: \(err)") 31 | } 32 | return nil 33 | } 34 | 35 | func testRequired() { 36 | guard let graph = getGraph(filename: "required") else { 37 | XCTFail("No graph object found.") 38 | return 39 | } 40 | 41 | XCTAssertEqual(graph.title, "Minimum required properties") 42 | XCTAssertEqual(graph.url, "http://examples.opengraphprotocol.us/required.html") 43 | XCTAssertEqual(graph.image.url, "http://examples.opengraphprotocol.us/media/images/50.png") 44 | XCTAssertNil(graph.image.mimeType) 45 | XCTAssertEqual(graph.determiner, .blank) 46 | 47 | XCTAssertTrue(graph.additionalImages.isEmpty) 48 | } 49 | 50 | func testOptional() { 51 | guard let graph = getGraph(filename: "optional") else { 52 | XCTFail("No graph object found.") 53 | return 54 | } 55 | 56 | XCTAssertEqual(graph.title, "Open Graph protocol examples") 57 | XCTAssertEqual(graph.url, "http://examples.opengraphprotocol.us/") 58 | XCTAssertEqual(graph.determiner, .the) 59 | XCTAssertEqual(graph.locale, "de_DE") 60 | XCTAssertEqual(graph.alternateLocales, ["en_US", "fr_FR"]) 61 | XCTAssertEqual(graph.image.url, "http://examples.opengraphprotocol.us/media/images/logo.png") 62 | XCTAssertEqual(graph.image.width, 300) 63 | XCTAssertEqual(graph.image.height, 300) 64 | XCTAssertEqual(graph.image.mimeType, "image/png") 65 | 66 | XCTAssertTrue(graph.additionalImages.isEmpty) 67 | } 68 | 69 | func testImageArray() { 70 | guard let graph = getGraph(filename: "image-array") else { 71 | XCTFail("No graph object found.") 72 | return 73 | } 74 | 75 | XCTAssertEqual(graph.title, "Two structured image properties") 76 | XCTAssertEqual(graph.siteName, "Open Graph protocol examples") 77 | XCTAssertEqual(graph.url, "http://examples.opengraphprotocol.us/image-array.html") 78 | 79 | XCTAssertEqual(graph.image.url, "http://examples.opengraphprotocol.us/media/images/75.png") 80 | XCTAssertEqual(graph.image.secureUrl, "https://d72cgtgi6hvvl.cloudfront.net/media/images/75.png") 81 | XCTAssertEqual(graph.image.width, 75) 82 | XCTAssertEqual(graph.image.height, 75) 83 | XCTAssertEqual(graph.image.mimeType, "image/png") 84 | XCTAssertEqual(graph.image.alt, "The first image, at 75x75px.") 85 | 86 | XCTAssertEqual(graph.additionalImages.count, 1) 87 | 88 | XCTAssertEqual(graph.additionalImages[0].url, "http://examples.opengraphprotocol.us/media/images/50.png") 89 | XCTAssertEqual(graph.additionalImages[0].secureUrl, "https://d72cgtgi6hvvl.cloudfront.net/media/images/50.png") 90 | XCTAssertEqual(graph.additionalImages[0].width, 50) 91 | XCTAssertEqual(graph.additionalImages[0].height, 50) 92 | XCTAssertEqual(graph.additionalImages[0].mimeType, "image/png") 93 | XCTAssertNil(graph.additionalImages[0].alt) 94 | 95 | switch graph.type { 96 | case .website: 97 | break 98 | default: 99 | XCTFail("Graph type was not album.") 100 | return 101 | } 102 | } 103 | 104 | func testImageURL() { 105 | guard let graph = getGraph(filename: "image-url") else { 106 | XCTFail("No graph object found.") 107 | return 108 | } 109 | 110 | XCTAssertEqual(graph.title, "Full structured image property") 111 | XCTAssertEqual(graph.siteName, "Open Graph protocol examples") 112 | XCTAssertEqual(graph.url, "http://examples.opengraphprotocol.us/image-url.html") 113 | 114 | XCTAssertEqual(graph.image.url, "http://examples.opengraphprotocol.us/media/images/50.png") 115 | XCTAssertEqual(graph.image.secureUrl, "https://d72cgtgi6hvvl.cloudfront.net/media/images/50.png") 116 | XCTAssertEqual(graph.image.width, 50) 117 | XCTAssertEqual(graph.image.height, 50) 118 | XCTAssertEqual(graph.image.mimeType, "image/png") 119 | 120 | XCTAssertTrue(graph.additionalImages.isEmpty) 121 | 122 | switch graph.type { 123 | case .website: 124 | break 125 | default: 126 | XCTFail("Graph type was not album.") 127 | return 128 | } 129 | } 130 | 131 | func testAudioArray() { 132 | guard let graph = getGraph(filename: "audio-array") else { 133 | XCTFail("No graph object found.") 134 | return 135 | } 136 | 137 | XCTAssertEqual(graph.title, "Two structured audio properties") 138 | XCTAssertEqual(graph.siteName, "Open Graph protocol examples") 139 | XCTAssertEqual(graph.url, "http://examples.opengraphprotocol.us/audio-array.html") 140 | 141 | XCTAssertEqual(graph.image.url, "http://examples.opengraphprotocol.us/media/images/50.png") 142 | XCTAssertEqual(graph.image.secureUrl, "https://d72cgtgi6hvvl.cloudfront.net/media/images/50.png") 143 | XCTAssertEqual(graph.image.width, 50) 144 | XCTAssertEqual(graph.image.height, 50) 145 | XCTAssertEqual(graph.image.mimeType, "image/png") 146 | 147 | XCTAssertEqual(graph.audios.count, 2) 148 | 149 | XCTAssertEqual(graph.audios[0].url, "http://examples.opengraphprotocol.us/media/audio/1khz.mp3") 150 | XCTAssertEqual(graph.audios[0].secureUrl, "https://d72cgtgi6hvvl.cloudfront.net/media/audio/1khz.mp3") 151 | XCTAssertEqual(graph.audios[0].mimeType, "audio/mpeg") 152 | XCTAssertNil(graph.audios[0].alt) 153 | 154 | XCTAssertEqual(graph.audios[1].url, "http://examples.opengraphprotocol.us/media/audio/250hz.mp3") 155 | XCTAssertEqual(graph.audios[1].secureUrl, "https://d72cgtgi6hvvl.cloudfront.net/media/audio/250hz.mp3") 156 | XCTAssertEqual(graph.audios[1].mimeType, "audio/mpeg") 157 | XCTAssertEqual(graph.audios[1].alt, "The second audio, at 250hz.") 158 | 159 | switch graph.type { 160 | case .website: 161 | break 162 | default: 163 | XCTFail("Graph type was not album.") 164 | return 165 | } 166 | } 167 | 168 | func testAudioURL() { 169 | guard let graph = getGraph(filename: "audio-url") else { 170 | XCTFail("No graph object found.") 171 | return 172 | } 173 | 174 | XCTAssertEqual(graph.title, "Structured audio property") 175 | XCTAssertEqual(graph.siteName, "Open Graph protocol examples") 176 | XCTAssertEqual(graph.url, "http://examples.opengraphprotocol.us/audio-url.html") 177 | 178 | XCTAssertEqual(graph.image.url, "http://examples.opengraphprotocol.us/media/images/50.png") 179 | XCTAssertEqual(graph.image.secureUrl, "https://d72cgtgi6hvvl.cloudfront.net/media/images/50.png") 180 | XCTAssertEqual(graph.image.width, 50) 181 | XCTAssertEqual(graph.image.height, 50) 182 | XCTAssertEqual(graph.image.mimeType, "image/png") 183 | 184 | XCTAssertEqual(graph.audios.count, 1) 185 | 186 | XCTAssertEqual(graph.audios[0].url, "http://examples.opengraphprotocol.us/media/audio/250hz.mp3") 187 | XCTAssertEqual(graph.audios[0].secureUrl, "https://d72cgtgi6hvvl.cloudfront.net/media/audio/250hz.mp3") 188 | XCTAssertEqual(graph.audios[0].mimeType, "audio/mpeg") 189 | XCTAssertNil(graph.audios[0].alt) 190 | 191 | switch graph.type { 192 | case .website: 193 | break 194 | default: 195 | XCTFail("Graph type was not album.") 196 | return 197 | } 198 | } 199 | 200 | func testVideoMovie() { 201 | guard let graph = getGraph(filename: "video-movie") else { 202 | XCTFail("No graph object found.") 203 | return 204 | } 205 | 206 | XCTAssertEqual(graph.title, "Arrival of a Train at La Ciotat") 207 | XCTAssertEqual(graph.description, "L'arrivée d'un train en gare de La Ciotat is an 1895 French short black-and-white silent documentary film directed and produced by Auguste and Louis Lumière. Its first public showing took place in January 1896.") 208 | XCTAssertEqual(graph.locale, "en_US") 209 | XCTAssertEqual(graph.url, "http://examples.opengraphprotocol.us/video-movie.html") 210 | 211 | XCTAssertEqual(graph.image.url, "http://examples.opengraphprotocol.us/media/images/train.jpg") 212 | XCTAssertEqual(graph.image.secureUrl, "https://d72cgtgi6hvvl.cloudfront.net/media/images/train.jpg") 213 | XCTAssertEqual(graph.image.width, 500) 214 | XCTAssertEqual(graph.image.height, 328) 215 | XCTAssertEqual(graph.image.mimeType, "image/jpeg") 216 | 217 | XCTAssertTrue(graph.audios.isEmpty) 218 | 219 | XCTAssertEqual(graph.videos.count, 3) 220 | 221 | XCTAssertEqual(graph.videos[0].url, "http://fpdownload.adobe.com/strobe/FlashMediaPlayback.swf?src=http%3A%2F%2Fexamples.opengraphprotocol.us%2Fmedia%2Fvideo%2Ftrain.mp4") 222 | XCTAssertEqual(graph.videos[0].secureUrl, "https://fpdownload.adobe.com/strobe/FlashMediaPlayback.swf?src=https%3A%2F%2Fd72cgtgi6hvvl.cloudfront.net%2Fmedia%2Fvideo%2Ftrain.mp4") 223 | XCTAssertEqual(graph.videos[0].mimeType, "application/x-shockwave-flash") 224 | XCTAssertEqual(graph.videos[0].width, 472) 225 | XCTAssertEqual(graph.videos[0].height, 296) 226 | XCTAssertNil(graph.videos[0].alt) 227 | 228 | XCTAssertEqual(graph.videos[1].mimeType, "video/mp4") 229 | XCTAssertEqual(graph.videos[1].width, 472) 230 | XCTAssertEqual(graph.videos[1].height, 296) 231 | XCTAssertNil(graph.videos[1].alt) 232 | 233 | XCTAssertEqual(graph.videos[2].mimeType, "video/webm") 234 | XCTAssertEqual(graph.videos[2].width, 480) 235 | XCTAssertEqual(graph.videos[2].height, 320) 236 | XCTAssertNil(graph.videos[2].alt) 237 | 238 | switch graph.type { 239 | case .video(let attributes): 240 | XCTAssertEqual(attributes.kind, .movie) 241 | let formatter = DateFormatter(dateStyle: .short) 242 | formatter.locale = .init(identifier: "en_US") 243 | XCTAssertEqual(formatter.string(from: attributes.releaseDate!), "12/28/95") 244 | XCTAssertEqual(attributes.directors, ["http://examples.opengraphprotocol.us/profile.html"]) 245 | XCTAssertEqual(attributes.duration, 50) 246 | XCTAssertEqual(attributes.tags, ["La Ciotat", "train"]) 247 | break 248 | default: 249 | XCTFail("Graph type was not album.") 250 | return 251 | } 252 | } 253 | 254 | func testMin() { 255 | XCTAssertNil(getGraph(filename: "min")) 256 | } 257 | 258 | func testMusicAlbum() async throws { 259 | let url = "https://music.apple.com/us/album/fallen-embers-deluxe-version/1591091543" 260 | let html = await getHtml(for: url) 261 | 262 | guard let graph = OpenGraph(html: html) else { 263 | XCTFail("No graph object found.") 264 | return 265 | } 266 | 267 | XCTAssertEqual(graph.title, "Fallen Embers (Deluxe Version) by ILLENIUM on Apple Music") 268 | XCTAssertEqual(graph.url, url) 269 | 270 | XCTAssertEqual(graph.image.url, "https://is1-ssl.mzstatic.com/image/thumb/Music122/v4/94/c2/14/94c21487-8703-4ae5-9386-17335d5e9f5d/093624874690.jpg/1200x1200bf-60.jpg") 271 | XCTAssertEqual(graph.image.secureUrl, "https://is1-ssl.mzstatic.com/image/thumb/Music122/v4/94/c2/14/94c21487-8703-4ae5-9386-17335d5e9f5d/093624874690.jpg/1200x1200bf-60.jpg") 272 | XCTAssertEqual(graph.image.alt, "Fallen Embers (Deluxe Version) by ILLENIUM on Apple Music") 273 | XCTAssertEqual(graph.image.width, 1200) 274 | XCTAssertEqual(graph.image.height, 1200) 275 | XCTAssertEqual(graph.image.mimeType, "image/jpg") 276 | 277 | switch graph.type { 278 | case .album(let album): 279 | XCTAssertEqual(album.songs.count, 19) 280 | XCTAssertEqual(album.songs[0].url, "https://music.apple.com/us/song/wouldnt-change-a-thing/1591091768") 281 | XCTAssertEqual(album.songs[0].disc, 1) 282 | XCTAssertEqual(album.songs[0].track, 1) 283 | XCTAssertEqual(album.songs[0].duration, 187) 284 | 285 | XCTAssertEqual(album.musician, "https://music.apple.com/us/artist/illenium/645420096") 286 | test(date: album.releaseDate, shortUSString: "10/22/21") 287 | 288 | default: 289 | XCTFail("Graph type was not album.") 290 | return 291 | } 292 | } 293 | } 294 | --------------------------------------------------------------------------------