├── Tests ├── Tests.swift └── Info.plist ├── Sources ├── OG.h ├── OGPreviewable.swift ├── TagTracker.swift ├── Parser.swift ├── OGTypeProtcols.swift └── OGTypes.swift ├── OG.playground ├── contents.xcplayground ├── playground.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Resources │ └── demo.html └── Contents.swift ├── .gitignore ├── OG.podspec ├── Package.swift ├── Info.plist ├── OG.xcworkspace └── contents.xcworkspacedata ├── LICENSE.md ├── OG.xcodeproj ├── xcshareddata │ └── xcschemes │ │ ├── OG-macOS.xcscheme │ │ └── OG-iOS.xcscheme └── project.pbxproj ├── CONDUCT.md └── README.md /Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import OG 3 | 4 | class Tests: XCTestCase {} 5 | -------------------------------------------------------------------------------- /Sources/OG.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | FOUNDATION_EXPORT double OGVersionNumber; 4 | FOUNDATION_EXPORT const unsigned char OGVersionString[]; 5 | -------------------------------------------------------------------------------- /OG.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | *build* 4 | 5 | *.svn 6 | 7 | *.DS_Store 8 | 9 | *.mode1v3 10 | *.pbxuser 11 | 12 | *.LSOverride 13 | 14 | *xcuserdata* 15 | 16 | *DerivedData* 17 | *.xcworkspacedata 18 | *.xcodeproj/project.xcworkspace 19 | -------------------------------------------------------------------------------- /OG.playground/playground.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OG.playground/Resources/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Rock (1996) 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /OG.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'OG' 3 | s.version = '1.4' 4 | s.license = 'BSD' 5 | s.summary = 'An OpenGraph parser in Swift' 6 | s.homepage = 'https://github.com/zadr/OG' 7 | s.authors = { 'Zachary Drayer' => 'zacharydrayer@gmail.com' } 8 | s.source = { :git => 'https://github.com/zadr/OG.git', :tag => s.version } 9 | 10 | s.ios.deployment_target = '8.0' 11 | s.osx.deployment_target = '10.9' 12 | s.tvos.deployment_target = '9.0' 13 | s.watchos.deployment_target = '2.0' 14 | 15 | s.source_files = 'Sources/*.swift' 16 | end 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "OG", 7 | platforms: [ 8 | .macOS(.v10_10), .iOS(.v10), .tvOS(.v9), .watchOS(.v2) 9 | ], 10 | products: [ 11 | .library(name: "OG", targets: ["OG"]), 12 | ], 13 | dependencies: [], 14 | targets: [ 15 | .target( 16 | name: "OG", 17 | dependencies: [], 18 | path: "Sources", 19 | exclude: ["Info.plist"] 20 | ), 21 | .testTarget( 22 | name: "Tests", 23 | dependencies: ["OG"], 24 | path: "Tests" 25 | ), 26 | ], 27 | swiftLanguageVersions: [.v5] 28 | ) 29 | -------------------------------------------------------------------------------- /OG.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import OG 2 | 3 | let testPath = Bundle.main.path(forResource: "demo", ofType: "html")! 4 | let testHTML = try! NSString(contentsOfFile: testPath, encoding: 4) as String 5 | print(testHTML) 6 | 7 | let parser = Parser() 8 | let tagTracker = TagTracker() 9 | parser.onFind = { (tag, values) in 10 | if !tagTracker.track(tag, values: values) { 11 | print("refusing to track non-meta tag \(tag) with values \(values)") 12 | } 13 | } 14 | 15 | let success = parser.parse(testHTML) 16 | if success { 17 | let tags = tagTracker.metadatum.compactMap(Metadata.from) 18 | print(tags) 19 | } else { 20 | print("parsing succeeded: \(success), unable to convert metadata \(tagTracker.metadatum) to OpenGraph object") 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.3.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /OG.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 26 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Simplified BSD License 2 | ====================== 3 | 4 | _Copyright © `2016-2017`, `Zachary Drayer`_ 5 | _All rights reserved._ 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those 28 | of the authors and should not be interpreted as representing official policies, 29 | either expressed or implied, of the OG Project. 30 | -------------------------------------------------------------------------------- /Sources/OGPreviewable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol OGPreviewable { 4 | /** 5 | Fetch data from a URL and attempt to parse OpenGraph tags out of it 6 | 7 | - Parameter session: The `URLSession` to make a data task on. Defaults to `URLSession.shared` 8 | - Parameter completion: A block to call when a data task finishes downloading and parsing data. The first parameter is a boolean that indicates if parsing succeeded or failed, and the second is an array of OpenGraph metadata types from a website 9 | 10 | // note: if this changes, change the documentation in the extension on `ReferenceConvertible` below 11 | */ 12 | func fetchOpenGraphData(session: URLSession, completion: @escaping ((Bool, [OGMetadata]?) -> Void)) 13 | } 14 | 15 | extension URL: OGPreviewable { 16 | public func fetchOpenGraphData(session: URLSession = .shared, completion: @escaping ((Bool, [OGMetadata]?) -> Void)) { 17 | URLRequest(url: self).fetchOpenGraphData(session: session, completion: completion) 18 | } 19 | } 20 | 21 | extension URLRequest: OGPreviewable { 22 | public func fetchOpenGraphData(session: URLSession = .shared, completion: @escaping ((Bool, [OGMetadata]?) -> Void)) { 23 | let task = session.dataTask(with: self) { (data, response, error) in 24 | if let data = data, let html = String(data: data, encoding: .utf8) { 25 | let parser = Parser() 26 | let tagTracker = TagTracker() 27 | 28 | parser.onFind = { (tag, values) in 29 | if !tagTracker.track(tag, values: values) { 30 | #if OG_DEBUG_LOGGING_ENABLED 31 | print("refusing to track non-meta tag \(tag) with values \(values)") 32 | #endif 33 | } 34 | } 35 | 36 | if parser.parse(html) { 37 | let graphs = tagTracker.metadatum.compactMap(Metadata.from) 38 | completion(true, graphs) 39 | } else { 40 | completion(false, nil) 41 | } 42 | } else { 43 | #if OG_DEBUG_LOGGING_ENABLED 44 | print("error \(String(describing: error)) on \(String(describing: response))") 45 | #endif 46 | 47 | completion(false, nil) 48 | } 49 | } 50 | task.resume() 51 | } 52 | } 53 | 54 | extension ReferenceConvertible where ReferenceType: OGPreviewable { 55 | /** 56 | Fetch data from a URL and attempt to parse OpenGraph tags out of it 57 | 58 | - Parameter session: The `URLSession` to make a data task on. Defaults to `URLSession.shared` 59 | - Parameter completion: A block to call when a data task finishes downloading and parsing data. The first parameter is a boolean that indicates if parsing succeeded or failed, and the second is an array of OpenGraph metadata types from a website 60 | 61 | note: if this changes, change the documentation in the protocol declaration of `OGPreviewable` above 62 | */ 63 | public func fetchOpenGraphData(session: URLSession = .shared, completion: @escaping ((Bool, [OGMetadata]?) -> Void)) { 64 | (self as! ReferenceType).fetchOpenGraphData(session: session, completion: completion) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /OG.xcodeproj/xcshareddata/xcschemes/OG-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at zacharydrayer@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /OG.xcodeproj/xcshareddata/xcschemes/OG-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Sources/TagTracker.swift: -------------------------------------------------------------------------------- 1 | /** 2 | `TagTracker` is a container that can keep track of OpenGraph tags as they come in. In order to fully support OpenGraph, 3 | and support pages with multiple OpenGraph elements, `TagTracker` keeps track of things as an array of dictionaries. 4 | */ 5 | public final class TagTracker { 6 | /** 7 | The raw OpenGraph metadata found on a given page. 8 | 9 | for example, after tracking parsing of follwing html, 10 | 11 | 12 | 13 | The Rock (1996) 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | `metadatum` will be 22 | 23 | [ 24 | "og:title": "The Rock", 25 | "og:type": "video.movie", 26 | "og:url": "http://www.imdb.com/title/tt0117500/", 27 | "og:image": "http://ia.media-imdb.com/images/rock.jpg" 28 | ] 29 | */ 30 | public fileprivate(set) lazy var metadatum: [[String: OpenGraphType]] = { 31 | var metadatum = [[String: OpenGraphType]]() 32 | metadatum.append([String: OpenGraphType]()) 33 | return metadatum 34 | }() 35 | 36 | /** 37 | The designated initializer for `TagTracker` 38 | */ 39 | public init() {} 40 | 41 | /** 42 | Used to ask TagTracker to track a new tag. 43 | 44 | The same tag can be tracked multiple times, and will result in an array of values being tracked. 45 | 46 | - parameter tag: The tag to track 47 | - parameter values: any values associated with the tag 48 | 49 | For example, to track the following html 50 | 51 | 52 | 53 | the `tag` parameter is `meta`, and the values are `["property": "og:title", "content": "The Rock"]` 54 | 55 | - returns: `true` if an OpenGraph meta tag with a property name and content was tracked, otherwise `false` 56 | */ 57 | public func track(_ tag: String, values: [String: String]) -> Bool { 58 | guard let tag = Tag(rawValue: tag), tag == .meta else { 59 | return false 60 | } 61 | 62 | var property: String? = nil 63 | var content: String? = nil 64 | 65 | for value in values { 66 | guard let pair = KeyValue(rawValue: value.0.lowercased()) else { 67 | continue 68 | } 69 | 70 | switch pair { 71 | case .property: 72 | property = value.1 73 | case .content: 74 | content = value.1 75 | } 76 | } 77 | 78 | var metadata = metadatum.popLast()! 79 | if let property = property, metadata[property] != nil && property == "og:type" { 80 | metadatum.append(metadata) 81 | metadata = [String: OpenGraphType]() 82 | } 83 | 84 | if let property = property, let content = content { 85 | if var existing = metadata[property] as? [OpenGraphType] { 86 | existing.append(property) 87 | metadata[property] = existing 88 | } else if let existing = metadata[property] { 89 | metadata[property] = [ existing, content ] 90 | } else { 91 | metadata[property] = content 92 | } 93 | } 94 | 95 | metadatum.append(metadata) 96 | 97 | return true 98 | } 99 | 100 | private enum Tag: RawRepresentable { 101 | typealias RawValue = String 102 | 103 | case head 104 | case meta 105 | 106 | init?(rawValue: RawValue) { 107 | switch rawValue.lowercased() { 108 | case "head": self = .head 109 | case "meta": self = .meta 110 | default: return nil 111 | } 112 | } 113 | 114 | var rawValue: RawValue { 115 | switch self { 116 | case .head: return "head" 117 | case .meta: return "meta" 118 | } 119 | } 120 | } 121 | 122 | private enum KeyValue: RawRepresentable { 123 | typealias RawValue = String 124 | 125 | case property 126 | case content 127 | 128 | init?(rawValue: RawValue) { 129 | switch rawValue.lowercased() { 130 | case "property": self = .property 131 | case "content": self = .content 132 | default: return nil 133 | } 134 | } 135 | 136 | var rawValue: RawValue { 137 | switch self { 138 | case .property: return "property" 139 | case .content: return "content" 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What is this? 2 | ===== 3 | 4 | OG is an [OpenGraph](https://ogp.me) parser in Swift. 5 | 6 | What's OpenGraph? 7 | ===== 8 | You know the smart previews of websites that appear on Facebook or Twitter? OpenGraph is the spec's used to help make that happen. 9 | 10 | Apple, recognizing that `OpenGraph` isn't the friendliest term, calls these previews [Link Previews](https://developer.apple.com/library/content/technotes/tn2444/_index.html). Hey, searchbot indexer, OG is a Link Preview Parser. 11 | 12 | What's in this? 13 | ===== 14 | 15 | Included is an Xcode Workspace containing an Xcodeproj for a dynamic library and an Xcode Playground to experiment with. 16 | 17 | What do I need to use this? 18 | ===== 19 | 20 | Requirements include iOS 8, Mac OS X 10.9, tvOS 9.0 or watchOS 2.0 and Swift 3.0 (included with Xcode 8.x) or Swift 4.0 (Included with Xcode 9.x). 21 | 22 | What do I do to start using this? 23 | ===== 24 | 25 | To start using OG, you can add it to your project by: 26 | - Using [Carthage](https://github.com/Carthage/Carthage) and adding `github "zadr/OG" ~> 1.3.3` to your `Cartfile`. 27 | - Using [CocoaPods](https://cocoapods.org) by adding `pod 'OG', '~1.3.3` to your `Podfile`. 28 | - Adding the current repository as a [git submodule](https://git-scm.com/docs/git-submodule), adding `OG.xcodeproj` into your `xcodeproj`, and adding `OG.framework` as an embedded binary to the Target of your project. 29 | 30 | What if I need help or want to help? 31 | ===== 32 | 33 | If you need any help, or find a bug, please [open an issue](https://github.com/zadr/OG/issues)! If you'd like to fix a bug or make any other contribution, feel free to open an issue, [make a pull request](https://github.com/zadr/OG/pulls), or [update the wiki](https://github.com/zadr/OG/wiki) with anything that you found helpful. 34 | 35 | What does using this look like? 36 | ===== 37 | 38 | There are two ways of using OG. 39 | 40 | The first way is to fetch metadata from a `URL` (or a `URLRequest`) automatically: 41 | 42 | ```swift 43 | if let url = URL(string: "https://…") { 44 | url.fetchOpenGraphData { (metadata) in 45 | print(metadata) 46 | } 47 | } 48 | ``` 49 | 50 | And the second way is more hands on; instead of fetching, parsing, and tracking tags automatically, OG exposes the components used for each step so you can pick and choose as needed. 51 | 52 | ```swift 53 | // first, fetch data from the network 54 | if let url = URL(string: "https://…") { 55 | // first fetch html that might have opengraph previews 56 | // The demo uses the built-in `URLSession`, but anything that can fetch data can be used here 57 | let task = URLSession.shared.dataTask(with: url) { (data, response, error) 58 | // make sure we successfully completed a request 59 | if let response = response as? HTTPURLResponse, response.statusCode >= 200, response.statusCode < 300 { 60 | if let data = data, let html = String(data: data, encoding: .utf8) { 61 | parse(html: html) 62 | } 63 | } 64 | } 65 | 66 | task.resume() 67 | } 68 | ``` 69 | 70 | 71 | ```swift 72 | // and then parse OpenGraph meta tags out of an html document 73 | func parse(html: String) { 74 | // then create a parser that can tell us the contents of each html tag and any associated key/value properties it has 75 | // `Parser` is provided, but this can be substituted with anything else that can iterate through html tags 76 | let parser = Parser() 77 | 78 | // and keep track of each tag as the parser encounters it 79 | // This could also be replaced with another component, but outside of testing purposes, there's less of an obvious need to do so than with the other steps of the process. 80 | let tagTracker = TagTracker() 81 | parser.onFind = { (tag, values) in 82 | if !tagTracker.track(tag, values: values) { 83 | print("refusing to track non-meta tag \(tag) with values \(values)") 84 | } 85 | } 86 | 87 | if parser.parse(html) { 88 | // - If we can parse html, map over our results to go from arrays of arrays of dictionaries (`[[String: OpenGraphType]]`) 89 | // to an array of OpenGraph objects. 90 | // - Note: OpenGraph can have multiple elements on a page (for example, an og:article, follwed by an og:author, followed by another og:author) 91 | let tags = tagTracker.metadatum.map(Metadata.from) 92 | print(tags) 93 | } 94 | } 95 | ``` 96 | 97 | What do I do after that? 98 | ===== 99 | Probably, put a preview of the website on screen. To help with this, every OpenGraph `Metadata` object has a `title`, an `imageUrl`, and a `url` of what to open upon tap or click. 100 | -------------------------------------------------------------------------------- /Sources/Parser.swift: -------------------------------------------------------------------------------- 1 | /** 2 | A barebones HTML parser that reports tags as it encounters them 3 | */ 4 | public final class Parser { 5 | private enum ParserState { 6 | case starting 7 | case matchingComment 8 | case matchingTagName 9 | case matchingPropertyName 10 | case matchingPropertyValue 11 | } 12 | 13 | /** 14 | A block to call whenever an HTML tag is encountered and successfully parsed. 15 | 16 | - block parameter 0: a `String` that represents the tag that was parsed 17 | - block parameter 1: a `Dictionary` containing all attributes found in the tag 18 | 19 | for example, after parsing 20 | 21 | 22 | 23 | `onFind` will be called with with the following arguments: 24 | 25 | ("tag", ["property": "og:title", "content": "The Rock"]) 26 | */ 27 | public var onFind: ((_ tag: String, _ value: [String: String]) -> Void)? = nil 28 | 29 | /** 30 | The designated initializer for `Parser` 31 | */ 32 | public init() {} 33 | 34 | /** 35 | Parse text and look for any HTML tags it contains. This does not perform any validation, such as checking if a tag appears inside of a tag. 36 | 37 | `onFind` must be set before any parsing will occur. 38 | 39 | - Parameter text: The text to parse. 40 | 41 | - Returns: `true` if the entire document is parseable, or `false` if an error was encountered. 42 | */ 43 | public func parse(_ text: String) -> Bool { 44 | guard let onFind = onFind else { 45 | return false 46 | } 47 | 48 | var values = [String: String]() 49 | 50 | var tagStack = [String]() 51 | var currentProperty = "" 52 | 53 | var inString = false 54 | var ignoreNextCharacter = false 55 | var ignoringUntilClosingTag = false 56 | var stack = String() 57 | var state: ParserState = .starting 58 | 59 | let matchedTagName = { 60 | state = .matchingPropertyName 61 | tagStack.append(stack) 62 | stack.removeAll() 63 | } 64 | 65 | let matchedKey = { 66 | state = .matchingPropertyValue 67 | currentProperty = stack 68 | stack.removeAll() 69 | } 70 | 71 | let matchedValue = { 72 | state = .matchingPropertyName 73 | values[currentProperty] = stack 74 | stack.removeAll() 75 | } 76 | 77 | for i in 0 ..< text.count - 1 { 78 | let characterStart = text.index(text.startIndex, offsetBy: i) 79 | let character = String(text[characterStart ..< text.index(after: characterStart)]) 80 | if !inString && character == ">" { 81 | if state == .matchingComment { 82 | state = .starting 83 | ignoringUntilClosingTag = false 84 | continue 85 | } else if state == .matchingTagName { 86 | matchedTagName() 87 | } else if state == .matchingPropertyValue { 88 | matchedValue() 89 | } 90 | 91 | state = .starting 92 | 93 | guard let currentTag = tagStack.popLast() else { 94 | return false 95 | } 96 | 97 | if !currentTag.isEmpty { 98 | onFind(currentTag, values) 99 | } 100 | 101 | values.removeAll() 102 | currentProperty = "" 103 | 104 | ignoringUntilClosingTag = false 105 | continue 106 | } 107 | 108 | if ignoringUntilClosingTag { 109 | continue 110 | } 111 | 112 | var didSetMatchingTagState = false 113 | if character == "<" { 114 | didSetMatchingTagState = true 115 | state = .matchingTagName 116 | } 117 | 118 | guard characterStart != text.endIndex else { 119 | break 120 | } 121 | 122 | let nextCharacterStart = text.index(after: characterStart) 123 | let nextCharacter = String(text[nextCharacterStart ..< text.index(after: nextCharacterStart)]) 124 | 125 | if !inString && nextCharacter == "/" { 126 | ignoringUntilClosingTag = true 127 | continue 128 | } 129 | 130 | if didSetMatchingTagState { 131 | if nextCharacter == "!" { 132 | state = .matchingComment 133 | ignoringUntilClosingTag = true 134 | } 135 | continue 136 | } 137 | 138 | if state == .starting { 139 | continue 140 | } 141 | 142 | stack.append(character) 143 | 144 | if character == "\\" { 145 | if ignoreNextCharacter { 146 | ignoreNextCharacter = false 147 | } else { 148 | ignoreNextCharacter = true 149 | stack.removeLast() 150 | continue 151 | } 152 | } else { 153 | ignoreNextCharacter = false 154 | } 155 | 156 | if !ignoreNextCharacter && character == "\"" { 157 | stack.removeLast() 158 | 159 | if !inString { 160 | inString = true 161 | continue 162 | } else { 163 | inString = false 164 | continue 165 | } 166 | } 167 | 168 | if (character != " " && character != "=") { 169 | continue 170 | } 171 | 172 | if state == .matchingTagName { 173 | stack.removeLast() 174 | matchedTagName() 175 | continue 176 | } 177 | 178 | if state == .matchingPropertyName { 179 | stack.removeLast() 180 | matchedKey() 181 | continue 182 | } 183 | 184 | if inString { continue } 185 | 186 | if state == .matchingPropertyValue { 187 | stack.removeLast() 188 | matchedValue() 189 | continue 190 | } 191 | } 192 | 193 | return tagStack.isEmpty 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Sources/OGTypeProtcols.swift: -------------------------------------------------------------------------------- 1 | /* 2 | - Property documentation (largely) copied from the OpenGraph Protocol spec, found at http://opengraphprotocol.org 3 | Which is licensed under the Open Web Foundation Agreement, Version 0.9, http://www.openwebfoundation.org/legal/the-0-9-agreements---necessary-claims/cla-copyright-grant-09-deed 4 | On May 14, 2017 5 | 6 | - There are some differences in data`OG` accepts vs what is specified in the OpenGraph protocol, however they all serve to expand on what the OpenGraph spec allows, rather than restrict. 7 | - All changes are released under the same license as the OpenGraph protocol 8 | - The things that were changed in OG's implementation vs the OpenGraph specification are: 9 | 1. `title`, `image` and `url` are considered required elements in the spec, their absence will not cause a parse failure 10 | 2. Some names were changed to be in line with the standard Cocoa convention found on iOS and macOS. For example, `image` is called `imageUrl`. 11 | 3. Some values (`musician` and `creator`) are arrays of Profile objects, rather than a singular Profile object. OG will still accept single Profile objects when encountered during parsing. 12 | 4. Profile.gender values are not tracked as closed enum (per the spec) and are instead treated as `String?` that may or may not exist, and may be set to any value. 13 | */ 14 | 15 | /// An empty protocol that's used to show that a type can be represented in OpenGraph form 16 | public protocol OpenGraphType {} 17 | extension Bool: OpenGraphType {} 18 | extension DateTime: OpenGraphType {} 19 | extension Double: OpenGraphType {} 20 | extension Int: OpenGraphType {} 21 | extension String: OpenGraphType {} 22 | 23 | // `Array` shouldn't be an `OpenGraphType` but is currently needed as an implementation detail of TagTracker 24 | extension Array: OpenGraphType {} 25 | 26 | // MARK: - 27 | 28 | /// Basic metadata that is common to every type that can be represented in OpenGraph form 29 | public protocol OGMetadata { 30 | init(values: [String: OpenGraphType]) 31 | 32 | /// The title of your object as it should appear within the graph, e.g., "The Rock". Required in OpenGraph spec but treated as an optional value. 33 | var title: String { get } 34 | 35 | /// An image URL which should represent your object within the graph. This has been renamed from `image` in the OpenGraph spec to better fit Cocoa idioms. Required in OpenGraph spec but treated as an optional value. 36 | var imageUrl: String { get } 37 | 38 | /// The canonical URL of your object that will be used as its permanent ID in the graph, e.g., "http://www.imdb.com/title/tt0117500/". Required in OpenGraph spec but treated as an optional value. 39 | var url: String { get } 40 | 41 | /// A URL to an audio file to accompany this object. This has been renamed from `audio` in the OpenGraph spec to better fit Cocoa idioms. 42 | var audioUrl: String? { get } 43 | 44 | /// A one to two sentence description of your object. This has been renamed from `description` in the OpenGraph spec to allow for objc compatibility. 45 | var graphDescription: String? { get } 46 | 47 | /// The word that appears before this object's title in a sentence. An enum of (a, an, the, "", auto). If auto is chosen, the consumer of your data should chose between "a" or "an". Default is "" (blank). 48 | var determiner: Determiner? { get } 49 | 50 | /// The locale these tags are marked up in. Of the format language_TERRITORY. Default is en_US. This has been renamed from `locale` in the OpenGraph spec to better fit Cocoa idioms. 51 | var localeString: String? { get } 52 | 53 | /// An array of other locales this page is available in. This has been renamed from `alternateLocales` in the OpenGraph spec to better fit Cocoa idioms. 54 | var alternateLocaleStrings: [String]? { get } 55 | 56 | /// If your object is part of a larger web site, the name which should be displayed for the overall site. e.g., "IMDb". 57 | var siteName: String? { get } 58 | 59 | /// A URL to a video file that complements this object. This has been renamed from `video` in the OpenGraph spec to better fit Cocoa idioms. 60 | var videoUrl: String? { get } 61 | } 62 | 63 | // MARK: - 64 | 65 | /// Extended metadata that may exist for any kind of multimedia 66 | public protocol OGMedia: OGMetadata { 67 | /// An alternate url to use if the webpage requires HTTPS. 68 | var secureUrl: String? { get } 69 | 70 | /// A MIME type for this image. 71 | var mimeType: String? { get } 72 | } 73 | 74 | /// Extended metadata that may exist for any kind of multimedia that is visually rendered (for example, an image or a movie). 75 | public protocol OGVisualMedia: OGMedia { 76 | /// The number of pixels wide. 77 | var width: Double? { get } 78 | 79 | /// The number of pixels high. 80 | var height: Double? { get } 81 | } 82 | 83 | /// An empty protocol for images 84 | public protocol OGImage: OGVisualMedia {} 85 | 86 | // MARK: - 87 | 88 | /// An empty protocol to serve as a base for any audio metadata 89 | public protocol OGMusic: OGMedia {} 90 | 91 | /// A song on an album or in a playlist 92 | public protocol OGSong: OGMusic { 93 | /// >=1 - The song's length in seconds. 94 | var duration: Int? { get } 95 | 96 | /// The album this song is from. A song can belong to multiple albums. 97 | var album: [OGAlbum]? { get } 98 | 99 | /// >=1 - Which disc of the album this song is on. 100 | var disc: Int? { get } 101 | 102 | /// >=1 - Which track this song is. 103 | var track: Int? { get } 104 | 105 | /// The musician that made this song. Multiple artists can be involved in one song. 106 | var musician: [OGProfile]? { get } 107 | } 108 | 109 | /// A song on an album or in a playlist 110 | public protocol OGAlbum: OGMusic { 111 | /// The song on this album. 112 | var song: OGSong? { get } 113 | 114 | /// >=1 - The same as music:album:disc but in reverse. 115 | var disc: Int? { get } 116 | 117 | /// >=1 - The same as music:album:track but in reverse. 118 | var track: Int? { get } 119 | 120 | /// The musician that made this song. This differs from the spec in being an array, rather than a single musician. 121 | var musician: [OGProfile]? { get } 122 | 123 | /// The date the album was released. 124 | var releaseDate: DateTime? { get } 125 | } 126 | 127 | /// A collection of songs made by any number of people 128 | public protocol OGPlaylist: OGMusic { 129 | /// The songs on this album. 130 | var song: [OGSong]? { get } 131 | 132 | /// >=1 - The same as music:album:disc but in reverse. 133 | var disc: Int? { get } 134 | 135 | /// >=1 - The same as music:album:track but in reverse. 136 | var track: Int? { get } 137 | 138 | /// The creator of this playlist. This differs from the spec in being an array, rather than a single musician. 139 | var creator: [OGProfile]? { get } 140 | } 141 | 142 | /// A song being broadcasted by a dj is a radio station in OpenGraph terms 143 | public protocol OGRadioStation: OGMusic { 144 | /// The creator of this playlist. This differs from the spec in being an array, rather than a single musician. 145 | var creator: [OGProfile]? { get } 146 | } 147 | 148 | // MARK: - 149 | 150 | /// Extended metadata that may exist for any kind of video metadata (for example, an episode of a tv show, a movie, or a youtube clip) 151 | public protocol OGVideo: OGVisualMedia { 152 | /// Actors in the movie. 153 | var actor: [OGProfile]? { get } 154 | 155 | /// The role they played. 156 | var roles: [String]? { get } 157 | 158 | /// Directors of the movie. 159 | var director: [OGProfile]? { get } 160 | 161 | /// Writers of the movie. 162 | var writer: [OGProfile]? { get } 163 | 164 | /// >=1 - The movie's length in seconds. 165 | var duration: Int? { get } 166 | 167 | /// The date the movie was released. 168 | var releaseDate: DateTime? { get } 169 | 170 | /// Tag words associated with this movie. 171 | var tag: [String]? { get } 172 | } 173 | 174 | /// An empty protocol for movies that conforms to `OGVideo` 175 | public protocol OGMovie: OGVideo {} 176 | 177 | /// A multi-episode TV show. The metadata is identical to `OGMovie`. 178 | public protocol OGTVShow: OGVideo {} 179 | 180 | /// A video that doesn't belong in any other category. The metadata is identical to `OGMovie`. 181 | public protocol OGOtherVideo: OGVideo {} 182 | 183 | /// An empty protocol for an episode of a tv show 184 | public protocol OGEpisode: OGVideo { 185 | /// Which series this episode belongs to. 186 | var series: OGTVShow? { get } 187 | } 188 | 189 | // MARK: - 190 | 191 | /// A protocol that represents an essay, blog post, paper, or any other collection of words 192 | public protocol OGArticle: OGMetadata { 193 | /// When the article was first published. 194 | var publishedTime: DateTime? { get } 195 | 196 | /// When the article was last changed. 197 | var modifiedTime: DateTime? { get } 198 | 199 | /// When the article is out of date after. 200 | var expirationTime: DateTime? { get } 201 | 202 | /// Writers of the article. 203 | var author: [OGProfile]? { get } 204 | 205 | /// A high-level section name. E.g. Technology 206 | var section: String? { get } 207 | 208 | /// Tag words associated with this article. 209 | var tag: [String]? { get } 210 | } 211 | 212 | /// A protocol that represents a published or unpublished book 213 | public protocol OGBook: OGMetadata { 214 | /// Who wrote this book. 215 | var author: [OGProfile]? { get } 216 | 217 | /// The ISBN(-10 or -13) of a book 218 | var isbn: String? { get } 219 | 220 | /// The date the book was released. 221 | var releaseDate: DateTime? { get } 222 | 223 | /// Tag words associated with this book. 224 | var tag: [String]? { get } 225 | } 226 | 227 | /// A protocol that represents details about an individual 228 | public protocol OGProfile: OGMetadata { 229 | /// A name normally given to an individual by a parent or self-chosen. 230 | var firstName: String? { get } 231 | 232 | /// A name inherited from a family or marriage and by which the individual is commonly known. 233 | var lastName: String? { get } 234 | 235 | /// A short unique string to identify them. 236 | var username: String? { get } 237 | 238 | /// Their gender. This differs from the spec in being a string that can contain any value, rather than a closed enum 239 | var gender: String? { get } 240 | } 241 | -------------------------------------------------------------------------------- /Sources/OGTypes.swift: -------------------------------------------------------------------------------- 1 | public class Metadata: OGMetadata { 2 | public fileprivate(set) var title: String = "" 3 | public fileprivate(set) var imageUrl: String = "" 4 | public fileprivate(set) var url: String = "" 5 | 6 | public fileprivate(set) var audioUrl: String? = nil 7 | public fileprivate(set) var graphDescription: String? = nil 8 | public fileprivate(set) var determiner: Determiner? = nil 9 | public fileprivate(set) var localeString: String? = nil 10 | public fileprivate(set) var alternateLocaleStrings: [String]? = nil 11 | public fileprivate(set) var siteName: String? = nil 12 | public fileprivate(set) var videoUrl: String? = nil 13 | 14 | public fileprivate(set) var rawData: [String: OpenGraphType] 15 | 16 | /** 17 | The designated initializer for OpenGraph metadata types. 18 | 19 | Direct usage of this is discouraged; `Metadata.from(_:)` will switch over any `og:type` and create the right class automatically. 20 | 21 | - parameter values: a dictionary of OpenGraph data 22 | */ 23 | public required init(values: [String: OpenGraphType]) { 24 | rawData = values 25 | 26 | if let title = values["og:title"] as? String { self.title = title } 27 | if let imageUrl = values["og:image"] as? String { self.imageUrl = imageUrl } 28 | if let url = values["og:url"] as? String { self.url = url } 29 | 30 | if let audioUrl = values["og:audio"] as? String { self.audioUrl = audioUrl } 31 | if let graphDescription = values["og:description"] as? String { self.graphDescription = graphDescription } 32 | if let determiner = values["og:determiner"] as? String { self.determiner = Determiner(rawValue: determiner) } 33 | if let locale = values["og:locale:alternate"] as? String { self.localeString = locale } 34 | if let alternateLocales = values["og:locale:alternate"] as? [String] { self.alternateLocaleStrings = alternateLocales } 35 | if let siteName = values["og:site_name"] as? String { self.siteName = siteName } 36 | if let videoUrl = values["og:video"] as? String { self.videoUrl = videoUrl } 37 | } 38 | 39 | /** 40 | A class function to create an `OpenGraph` class for any supported OpenGraph type of object. 41 | 42 | - parameter values: a dictionary of OpenGraph data 43 | - returns: `nil` if `og:type` isn't specified in the `values` dictionary, otherwise an OpenGraph object type 44 | */ 45 | public class func from(_ values: [String: OpenGraphType]) -> Metadata? { 46 | guard let type = values["og:type"] as? String else { return nil } 47 | 48 | switch type { 49 | case "music.song": return Song(values: values) 50 | case "music.album": return Album(values: values) 51 | case "music.playlist": return Playlist(values: values) 52 | case "music.radio_station": return RadioStation(values: values) 53 | case "video.movie": return Movie(values: values) 54 | case "video.episode": return Episode(values: values) 55 | case "video.tv_show": return TVShow(values: values) 56 | case "video.other": return OtherVideo(values: values) 57 | case "article": return Article(values: values) 58 | case "book": return Book(values: values) 59 | case "profile": return Profile(values: values) 60 | default: return Metadata(values: values) 61 | } 62 | } 63 | } 64 | 65 | extension Metadata: CustomStringConvertible { 66 | public var description: String { 67 | var mirrors = [Mirror]() 68 | var mirror: Mirror? = Mirror(reflecting: self) 69 | var description = "<\(Unmanaged.passUnretained(self).toOpaque())> \(mirror!.subjectType): {" 70 | 71 | while mirror != nil { 72 | mirrors.insert(mirror!, at: 0) 73 | mirror = mirror!.superclassMirror 74 | } 75 | 76 | for i in 0 ..< mirrors.count { 77 | let mirror = mirrors[i] 78 | let tabsForLevel = String(repeating: "\t", count: i + 1) 79 | 80 | description += mirror.children.compactMap { 81 | guard let key = $0.label else { return nil } 82 | return "\n \(tabsForLevel)\(key): \($0.value)," 83 | } .reduce("") { return $0 + $1 } 84 | 85 | description += "\n\(tabsForLevel)\(mirror.subjectType): {" 86 | } 87 | 88 | for i in stride(from: mirrors.count, to: 0, by: -1) { 89 | description += "\n\(String(repeating: "\t", count: i))}" 90 | } 91 | 92 | return description + "\n}" 93 | } 94 | } 95 | 96 | // MARK: - 97 | 98 | public class Media: Metadata, OGMedia { 99 | public fileprivate(set) var secureUrl: String? = nil 100 | public fileprivate(set) var mimeType: String? = nil 101 | } 102 | 103 | public class VisualMedia: Media, OGVisualMedia { 104 | public fileprivate(set) var width: Double? = nil 105 | public fileprivate(set) var height: Double? = nil 106 | } 107 | 108 | // MARK: - 109 | 110 | public final class Image: VisualMedia, OGImage { 111 | public required init(values: [String: OpenGraphType]) { 112 | super.init(values: values) 113 | 114 | if let url = values["og:image"] as? String { self.url = url } 115 | if let url = values["og:image:url"] as? String { self.url = url } 116 | 117 | self.secureUrl = values["og:image:secure_url"] as? String 118 | self.mimeType = values["og:image:type"] as? String 119 | 120 | if let width = values["og:image:width"] as? String { self.width = Double(width) } 121 | if let height = values["og:image:height"] as? String { self.height = Double(height) } 122 | } 123 | } 124 | 125 | // MARK: - 126 | 127 | public class Music: Media, OGMusic { 128 | public required init(values: [String: OpenGraphType]) { 129 | super.init(values: values) 130 | 131 | if let url = values["og:audio"] as? String { self.url = url } 132 | if let url = values["og:audio:url"] as? String { self.url = url } 133 | if let secureUrl = values["og:audio:secure_url"] as? String { self.secureUrl = secureUrl } 134 | if let mimeType = values["og:audio:type"] as? String { self.mimeType = mimeType } 135 | } 136 | } 137 | 138 | public final class Song: Music, OGSong { 139 | public fileprivate(set) var duration: Int? = nil 140 | public fileprivate(set) var album: [OGAlbum]? = nil 141 | public fileprivate(set) var disc: Int? = nil 142 | public fileprivate(set) var track: Int? = nil 143 | public fileprivate(set) var musician: [OGProfile]? = nil 144 | 145 | public required init(values: [String: OpenGraphType]) { 146 | super.init(values: values) 147 | 148 | if let duration = values["og:music:duration"] as? String { self.duration = Int(duration) } 149 | if let albums = values["og:music:album"] as? [[String: OpenGraphType]] { self.album = albums.map { return Album(values: $0) } } 150 | if let disc = values["og:music:album:disc"] as? String { self.disc = Int(disc) } 151 | if let track = values["og:music:track"] as? String { self.track = Int(track) } 152 | if let musician = values["og:music:musician"] as? [[String: OpenGraphType]] { self.musician = musician.map { return Profile(values: $0) } } 153 | else if let musician = values["og:music:musician"] as? [String: OpenGraphType] { self.musician = [ Profile(values: musician) ] } 154 | } 155 | } 156 | 157 | public final class Album: Music, OGAlbum { 158 | public fileprivate(set) var song: OGSong? = nil 159 | public fileprivate(set) var disc: Int? = nil 160 | public fileprivate(set) var track: Int? = nil 161 | public fileprivate(set) var musician: [OGProfile]? = nil 162 | public fileprivate(set) var releaseDate: DateTime? = nil 163 | 164 | public required init(values: [String: OpenGraphType]) { 165 | super.init(values: values) 166 | 167 | if let song = values["og:music:song"] as? [String: OpenGraphType] { self.song = Song(values: song) } 168 | if let disc = values["og:music:album:disc"] as? String { self.disc = Int(disc) } 169 | if let track = values["og:music:track"] as? String { self.track = Int(track) } 170 | if let musician = values["og:music:musician"] as? [[String: OpenGraphType]] { self.musician = musician.map { return Profile(values: $0) } } 171 | else if let musician = values["og:music:musician"] as? [String: OpenGraphType] { self.musician = [ Profile(values: musician) ] } 172 | if let releaseDate = values["og:music:release_date"] as? String { self.releaseDate = DateTime(value: releaseDate) } 173 | } 174 | } 175 | 176 | public final class Playlist: Music, OGPlaylist { 177 | public fileprivate(set) var song: [OGSong]? = nil 178 | public fileprivate(set) var disc: Int? = nil 179 | public fileprivate(set) var track: Int? = nil 180 | public fileprivate(set) var creator: [OGProfile]? = nil 181 | 182 | public required init(values: [String: OpenGraphType]) { 183 | super.init(values: values) 184 | 185 | if let song = values["og:music:song"] as? [[String: OpenGraphType]] { self.song = song.map { return Song(values: $0) } } 186 | if let disc = values["og:music:album:disc"] as? String { self.disc = Int(disc) } 187 | if let track = values["og:music:track"] as? String { self.track = Int(track) } 188 | if let creator = values["og:music:creator"] as? [[String: OpenGraphType]] { self.creator = creator.map { return Profile(values: $0) } } 189 | else if let creator = values["og:music:creator"] as? [String: OpenGraphType] { self.creator = [ Profile(values: creator) ] } 190 | } 191 | } 192 | 193 | public final class RadioStation: Music, OGRadioStation { 194 | public fileprivate(set) var creator: [OGProfile]? = nil 195 | 196 | public required init(values: [String: OpenGraphType]) { 197 | super.init(values: values) 198 | 199 | if let creator = values["og:music:creator"] as? [[String: OpenGraphType]] { self.creator = creator.map { return Profile(values: $0) } } 200 | else if let creator = values["og:music:creator"] as? [String: OpenGraphType] { self.creator = [ Profile(values: creator) ] } 201 | } 202 | } 203 | 204 | // MARK: - 205 | 206 | public class Video: VisualMedia { 207 | public fileprivate(set) var actor: [OGProfile]? = nil 208 | public fileprivate(set) var roles: [String]? = nil 209 | public fileprivate(set) var director: [OGProfile]? = nil 210 | public fileprivate(set) var writer: [OGProfile]? = nil 211 | public fileprivate(set) var duration: Int? = nil 212 | public fileprivate(set) var releaseDate: DateTime? = nil 213 | public fileprivate(set) var tag: [String]? = nil 214 | 215 | public required init(values: [String: OpenGraphType]) { 216 | super.init(values: values) 217 | 218 | if let url = values["og:video"] as? String { self.url = url } 219 | if let url = values["og:video:url"] as? String { self.url = url } 220 | if let secureUrl = values["og:video:secure_url"] as? String { self.secureUrl = secureUrl } 221 | if let mimeType = values["og:video:type"] as? String { self.mimeType = mimeType } 222 | if let width = values["og:video:width"] as? String { self.width = Double(width) } 223 | if let height = values["og:video:height"] as? String { self.height = Double(height) } 224 | 225 | if let actors = values["og:video:actor"] as? [[String: OpenGraphType]] { self.actor = actors.map { return Profile(values: $0) } } 226 | if let roles = values["og:video:actor:role"] as? [String] { self.roles = roles } 227 | if let directors = values["og:video:director"] as? [[String: OpenGraphType]] { self.director = directors.map { return Profile(values: $0) } } 228 | if let writers = values["og:video:writer"] as? [[String: OpenGraphType]] { self.writer = writers.map { return Profile(values: $0) } } 229 | if let duration = values["og:video:duration"] as? String { self.duration = Int(duration) } 230 | if let releaseDate = values["og:video:release_date"] as? String { self.releaseDate = DateTime(value: releaseDate) } 231 | if let tags = values["og:video:tag"] as? [String] { self.tag = tags } 232 | } 233 | } 234 | 235 | public final class Movie: Video, OGMovie {} 236 | public final class TVShow: Video, OGTVShow {} 237 | public final class OtherVideo: Video, OGOtherVideo {} 238 | public final class Episode: Video, OGEpisode { 239 | public fileprivate(set) var series: OGTVShow? = nil 240 | 241 | public required init(values: [String: OpenGraphType]) { 242 | super.init(values: values) 243 | 244 | if let series = values["og:video:series"] as? [String: OpenGraphType] { self.series = TVShow(values: series) } 245 | } 246 | } 247 | 248 | // MARK: - 249 | 250 | public final class Article: Metadata, OGArticle { 251 | public fileprivate(set) var publishedTime: DateTime? = nil 252 | public fileprivate(set) var modifiedTime: DateTime? = nil 253 | public fileprivate(set) var expirationTime: DateTime? = nil 254 | public fileprivate(set) var author: [OGProfile]? = nil 255 | public fileprivate(set) var section: String? = nil 256 | public fileprivate(set) var tag: [String]? = nil 257 | 258 | public required init(values: [String: OpenGraphType]) { 259 | super.init(values: values) 260 | 261 | if let publishedTime = values["og:article:published_time"] as? String { self.publishedTime = DateTime(value: publishedTime) } 262 | if let modifiedTime = values["og:article:modified_time"] as? String { self.modifiedTime = DateTime(value: modifiedTime) } 263 | if let expirationTime = values["og:article:expiration_time"] as? String { self.expirationTime = DateTime(value: expirationTime) } 264 | if let authors = values["og:article:author"] as? [[String: OpenGraphType]] { self.author = authors.map { return Profile(values: $0) } } 265 | if let section = values["og:article:section"] as? String { self.section = section } 266 | if let tags = values["og:article:tag"] as? [String] { self.tag = tags } 267 | } 268 | } 269 | 270 | public final class Book: Metadata, OGBook { 271 | public fileprivate(set) var author: [OGProfile]? = nil 272 | public fileprivate(set) var isbn: String? = nil 273 | public fileprivate(set) var releaseDate: DateTime? = nil 274 | public fileprivate(set) var tag: [String]? = nil 275 | 276 | public required init(values: [String: OpenGraphType]) { 277 | super.init(values: values) 278 | 279 | if let authors = values["og:book:author"] as? [[String: OpenGraphType]] { self.author = authors.map { return Profile(values: $0) } } 280 | if let isbn = values["og:book:isbn"] as? String { self.isbn = isbn } 281 | if let releaseDate = values["og:book:release_date"] as? String { self.releaseDate = DateTime(value: releaseDate) } 282 | if let tags = values["og:book:tag"] as? [String] { self.tag = tags } 283 | } 284 | } 285 | 286 | public final class Profile: Metadata, OGProfile { 287 | public fileprivate(set) var firstName: String? = nil 288 | public fileprivate(set) var lastName: String? = nil 289 | public fileprivate(set) var username: String? = nil 290 | public fileprivate(set) var gender: String? = nil 291 | 292 | public required init(values: [String: OpenGraphType]) { 293 | super.init(values: values) 294 | 295 | if let firstName = values["og:profile:first_name"] as? String { self.firstName = firstName } 296 | if let lastName = values["og:profile:last_name"] as? String { self.lastName = lastName } 297 | if let username = values["og:profile:username"] as? String { self.username = username } 298 | if let gender = values["og:profile:gender"] as? String { self.gender = gender } 299 | } 300 | } 301 | 302 | /// A DateTime represents a temporal value composed of a date (year, month, day) and an optional time component (hours, minutes) 303 | public struct DateTime { 304 | fileprivate enum DateTimeProperty { 305 | case year 306 | case month 307 | case day 308 | case hours 309 | case minutes 310 | } 311 | 312 | var year: Int 313 | var month: Int 314 | var day: Int 315 | 316 | var hours: Int? 317 | var minutes: Int? 318 | 319 | /** 320 | Create a `DateTime` object for a given temporal value. 321 | 322 | - parameter value: An ISO8601-formatted string to be parsed into a date. 323 | */ 324 | init(value: String) { 325 | var year: Int? = nil 326 | var month: Int? = nil 327 | var day: Int? = nil 328 | 329 | var current = "" 330 | var property = DateTimeProperty.year 331 | for i in 0 ..< current.utf8.count { 332 | var previousEnding: String? = nil 333 | var length: Int = 0 334 | 335 | switch property { 336 | case .year: 337 | previousEnding = "" 338 | length = 4 339 | case .month: 340 | previousEnding = "-" 341 | length = 2 342 | case .day: 343 | previousEnding = "-" 344 | length = 2 345 | case .hours: 346 | previousEnding = "T" 347 | length = 2 348 | case .minutes: 349 | previousEnding = ":" 350 | length = 2 351 | } 352 | 353 | 354 | let characterStart = current.index(current.startIndex, offsetBy: i) 355 | let character = String(current[characterStart ..< current.index(after: characterStart)]) 356 | 357 | if character == previousEnding { 358 | continue 359 | } 360 | 361 | if current.count == length { 362 | switch property { 363 | case .year: 364 | year = Int(current)! 365 | property = .month 366 | case .month: 367 | month = Int(current)! 368 | property = .day 369 | case .day: 370 | day = Int(current)! 371 | property = .hours 372 | case .hours: 373 | self.hours = Int(current) 374 | property = .minutes 375 | case .minutes: 376 | self.minutes = Int(current) 377 | } 378 | } else { 379 | current.append(character) 380 | } 381 | } 382 | 383 | self.year = year! 384 | self.month = month! 385 | self.day = day! 386 | } 387 | } 388 | 389 | // MARK: - 390 | 391 | /// The word that appears before this object's title in a sentence. An enum of (a, an, the, "", auto). If auto is chosen, the consumer of your data should chose between "a" or "an". Default is "" (blank). 392 | public enum Determiner: RawRepresentable { 393 | public typealias RawValue = String 394 | 395 | case a 396 | case an 397 | case blank 398 | case the 399 | case quotes 400 | case auto 401 | 402 | public init?(rawValue: RawValue) { 403 | switch rawValue.lowercased() { 404 | case "a": self = .a 405 | case "an": self = .an 406 | case "": self = .blank 407 | case "the": self = .the 408 | case "\"": self = .quotes 409 | case "'": self = .quotes 410 | case "‘": self = .quotes 411 | case "’": self = .quotes 412 | case "“": self = .quotes 413 | case "”": self = .quotes 414 | case "auto": self = .auto 415 | default: return nil 416 | } 417 | } 418 | 419 | public var rawValue: RawValue { 420 | switch self { 421 | case .a: return "a" 422 | case .an: return "an" 423 | case .blank: return "" 424 | case .the: return "the" 425 | case .quotes: return "\"" 426 | case .auto: return "auto" 427 | } 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /OG.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B6261D931EC93DDA00706C24 /* OGPreviewable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6261D921EC93DDA00706C24 /* OGPreviewable.swift */; }; 11 | B6261D961EC94B4500706C24 /* OGPreviewable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6261D921EC93DDA00706C24 /* OGPreviewable.swift */; }; 12 | B6261D971EC94B4500706C24 /* OGTypeProtcols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B9AB11D29FD97001EBF97 /* OGTypeProtcols.swift */; }; 13 | B6261D981EC94B4500706C24 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B9AB41D29FD98001EBF97 /* Parser.swift */; }; 14 | B6261D991EC94B4500706C24 /* OGTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B9AB31D29FD98001EBF97 /* OGTypes.swift */; }; 15 | B6261D9B1EC94B4500706C24 /* TagTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DC94E41D2CBDBF00C9A005 /* TagTracker.swift */; }; 16 | B6261D9E1EC94B4500706C24 /* OG.h in Headers */ = {isa = PBXBuildFile; fileRef = B69B9ABA1D29FD9F001EBF97 /* OG.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17 | B69B9A8F1D29F3CC001EBF97 /* OG.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B69B9A841D29F3CC001EBF97 /* OG.framework */; }; 18 | B69B9AB01D29FC72001EBF97 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B9AAF1D29FC72001EBF97 /* Tests.swift */; }; 19 | B69B9AB51D29FD98001EBF97 /* OGTypeProtcols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B9AB11D29FD97001EBF97 /* OGTypeProtcols.swift */; }; 20 | B69B9AB71D29FD98001EBF97 /* OGTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B9AB31D29FD98001EBF97 /* OGTypes.swift */; }; 21 | B69B9AB81D29FD98001EBF97 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B9AB41D29FD98001EBF97 /* Parser.swift */; }; 22 | B69B9ABC1D29FD9F001EBF97 /* OG.h in Headers */ = {isa = PBXBuildFile; fileRef = B69B9ABA1D29FD9F001EBF97 /* OG.h */; settings = {ATTRIBUTES = (Public, ); }; }; 23 | B6DC94E51D2CBDBF00C9A005 /* TagTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DC94E41D2CBDBF00C9A005 /* TagTracker.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | B69B9A901D29F3CC001EBF97 /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = B69B9A7B1D29F3CC001EBF97 /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = B69B9A831D29F3CC001EBF97; 32 | remoteInfo = OG; 33 | }; 34 | /* End PBXContainerItemProxy section */ 35 | 36 | /* Begin PBXFileReference section */ 37 | B6261D921EC93DDA00706C24 /* OGPreviewable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OGPreviewable.swift; path = Sources/OGPreviewable.swift; sourceTree = SOURCE_ROOT; }; 38 | B6261DA31EC94B4500706C24 /* OG.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OG.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | B69B9A841D29F3CC001EBF97 /* OG.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OG.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | B69B9A8E1D29F3CC001EBF97 /* OGTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OGTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | B69B9A951D29F3CC001EBF97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | B69B9AAF1D29FC72001EBF97 /* Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Tests.swift; path = Tests/Tests.swift; sourceTree = SOURCE_ROOT; }; 43 | B69B9AB11D29FD97001EBF97 /* OGTypeProtcols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OGTypeProtcols.swift; path = Sources/OGTypeProtcols.swift; sourceTree = SOURCE_ROOT; }; 44 | B69B9AB31D29FD98001EBF97 /* OGTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OGTypes.swift; path = Sources/OGTypes.swift; sourceTree = SOURCE_ROOT; }; 45 | B69B9AB41D29FD98001EBF97 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Parser.swift; path = Sources/Parser.swift; sourceTree = SOURCE_ROOT; }; 46 | B69B9ABA1D29FD9F001EBF97 /* OG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OG.h; path = Sources/OG.h; sourceTree = SOURCE_ROOT; }; 47 | B69B9ABD1D29FDC2001EBF97 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; 48 | B6DC94E41D2CBDBF00C9A005 /* TagTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TagTracker.swift; path = Sources/TagTracker.swift; sourceTree = SOURCE_ROOT; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | B6261D9C1EC94B4500706C24 /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | B69B9A801D29F3CC001EBF97 /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | ); 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | B69B9A8B1D29F3CC001EBF97 /* Frameworks */ = { 67 | isa = PBXFrameworksBuildPhase; 68 | buildActionMask = 2147483647; 69 | files = ( 70 | B69B9A8F1D29F3CC001EBF97 /* OG.framework in Frameworks */, 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXFrameworksBuildPhase section */ 75 | 76 | /* Begin PBXGroup section */ 77 | B69B9A7A1D29F3CC001EBF97 = { 78 | isa = PBXGroup; 79 | children = ( 80 | B69B9A861D29F3CC001EBF97 /* Framework */, 81 | B69B9A921D29F3CC001EBF97 /* Tests */, 82 | B69B9A851D29F3CC001EBF97 /* Products */, 83 | ); 84 | sourceTree = ""; 85 | }; 86 | B69B9A851D29F3CC001EBF97 /* Products */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | B69B9A841D29F3CC001EBF97 /* OG.framework */, 90 | B69B9A8E1D29F3CC001EBF97 /* OGTests.xctest */, 91 | B6261DA31EC94B4500706C24 /* OG.framework */, 92 | ); 93 | name = Products; 94 | sourceTree = ""; 95 | }; 96 | B69B9A861D29F3CC001EBF97 /* Framework */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | B6261D921EC93DDA00706C24 /* OGPreviewable.swift */, 100 | B69B9AB11D29FD97001EBF97 /* OGTypeProtcols.swift */, 101 | B69B9AB31D29FD98001EBF97 /* OGTypes.swift */, 102 | B69B9AB41D29FD98001EBF97 /* Parser.swift */, 103 | B6DC94E41D2CBDBF00C9A005 /* TagTracker.swift */, 104 | B69B9ABA1D29FD9F001EBF97 /* OG.h */, 105 | B69B9ABD1D29FDC2001EBF97 /* Info.plist */, 106 | ); 107 | name = Framework; 108 | path = OG; 109 | sourceTree = ""; 110 | }; 111 | B69B9A921D29F3CC001EBF97 /* Tests */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | B69B9AAF1D29FC72001EBF97 /* Tests.swift */, 115 | B69B9A951D29F3CC001EBF97 /* Info.plist */, 116 | ); 117 | name = Tests; 118 | path = OGTests; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXHeadersBuildPhase section */ 124 | B6261D9D1EC94B4500706C24 /* Headers */ = { 125 | isa = PBXHeadersBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | B6261D9E1EC94B4500706C24 /* OG.h in Headers */, 129 | ); 130 | runOnlyForDeploymentPostprocessing = 0; 131 | }; 132 | B69B9A811D29F3CC001EBF97 /* Headers */ = { 133 | isa = PBXHeadersBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | B69B9ABC1D29FD9F001EBF97 /* OG.h in Headers */, 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | /* End PBXHeadersBuildPhase section */ 141 | 142 | /* Begin PBXNativeTarget section */ 143 | B6261D941EC94B4500706C24 /* OG-macOS */ = { 144 | isa = PBXNativeTarget; 145 | buildConfigurationList = B6261DA01EC94B4500706C24 /* Build configuration list for PBXNativeTarget "OG-macOS" */; 146 | buildPhases = ( 147 | B6261D951EC94B4500706C24 /* Sources */, 148 | B6261D9C1EC94B4500706C24 /* Frameworks */, 149 | B6261D9D1EC94B4500706C24 /* Headers */, 150 | B6261D9F1EC94B4500706C24 /* Resources */, 151 | ); 152 | buildRules = ( 153 | ); 154 | dependencies = ( 155 | ); 156 | name = "OG-macOS"; 157 | productName = OG; 158 | productReference = B6261DA31EC94B4500706C24 /* OG.framework */; 159 | productType = "com.apple.product-type.framework"; 160 | }; 161 | B69B9A831D29F3CC001EBF97 /* OG-iOS */ = { 162 | isa = PBXNativeTarget; 163 | buildConfigurationList = B69B9A981D29F3CC001EBF97 /* Build configuration list for PBXNativeTarget "OG-iOS" */; 164 | buildPhases = ( 165 | B69B9A7F1D29F3CC001EBF97 /* Sources */, 166 | B69B9A801D29F3CC001EBF97 /* Frameworks */, 167 | B69B9A811D29F3CC001EBF97 /* Headers */, 168 | B69B9A821D29F3CC001EBF97 /* Resources */, 169 | ); 170 | buildRules = ( 171 | ); 172 | dependencies = ( 173 | ); 174 | name = "OG-iOS"; 175 | productName = OG; 176 | productReference = B69B9A841D29F3CC001EBF97 /* OG.framework */; 177 | productType = "com.apple.product-type.framework"; 178 | }; 179 | B69B9A8D1D29F3CC001EBF97 /* OGTests */ = { 180 | isa = PBXNativeTarget; 181 | buildConfigurationList = B69B9A9B1D29F3CC001EBF97 /* Build configuration list for PBXNativeTarget "OGTests" */; 182 | buildPhases = ( 183 | B69B9A8A1D29F3CC001EBF97 /* Sources */, 184 | B69B9A8B1D29F3CC001EBF97 /* Frameworks */, 185 | B69B9A8C1D29F3CC001EBF97 /* Resources */, 186 | ); 187 | buildRules = ( 188 | ); 189 | dependencies = ( 190 | B69B9A911D29F3CC001EBF97 /* PBXTargetDependency */, 191 | ); 192 | name = OGTests; 193 | productName = OGTests; 194 | productReference = B69B9A8E1D29F3CC001EBF97 /* OGTests.xctest */; 195 | productType = "com.apple.product-type.bundle.unit-test"; 196 | }; 197 | /* End PBXNativeTarget section */ 198 | 199 | /* Begin PBXProject section */ 200 | B69B9A7B1D29F3CC001EBF97 /* Project object */ = { 201 | isa = PBXProject; 202 | attributes = { 203 | LastSwiftUpdateCheck = 0730; 204 | LastUpgradeCheck = 0730; 205 | ORGANIZATIONNAME = Zachary; 206 | TargetAttributes = { 207 | B69B9A831D29F3CC001EBF97 = { 208 | CreatedOnToolsVersion = 7.3; 209 | LastSwiftMigration = 0820; 210 | }; 211 | B69B9A8D1D29F3CC001EBF97 = { 212 | CreatedOnToolsVersion = 7.3; 213 | LastSwiftMigration = 0820; 214 | }; 215 | }; 216 | }; 217 | buildConfigurationList = B69B9A7E1D29F3CC001EBF97 /* Build configuration list for PBXProject "OG" */; 218 | compatibilityVersion = "Xcode 3.2"; 219 | developmentRegion = English; 220 | hasScannedForEncodings = 0; 221 | knownRegions = ( 222 | en, 223 | ); 224 | mainGroup = B69B9A7A1D29F3CC001EBF97; 225 | productRefGroup = B69B9A851D29F3CC001EBF97 /* Products */; 226 | projectDirPath = ""; 227 | projectRoot = ""; 228 | targets = ( 229 | B69B9A831D29F3CC001EBF97 /* OG-iOS */, 230 | B6261D941EC94B4500706C24 /* OG-macOS */, 231 | B69B9A8D1D29F3CC001EBF97 /* OGTests */, 232 | ); 233 | }; 234 | /* End PBXProject section */ 235 | 236 | /* Begin PBXResourcesBuildPhase section */ 237 | B6261D9F1EC94B4500706C24 /* Resources */ = { 238 | isa = PBXResourcesBuildPhase; 239 | buildActionMask = 2147483647; 240 | files = ( 241 | ); 242 | runOnlyForDeploymentPostprocessing = 0; 243 | }; 244 | B69B9A821D29F3CC001EBF97 /* Resources */ = { 245 | isa = PBXResourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | B69B9A8C1D29F3CC001EBF97 /* Resources */ = { 252 | isa = PBXResourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | /* End PBXResourcesBuildPhase section */ 259 | 260 | /* Begin PBXSourcesBuildPhase section */ 261 | B6261D951EC94B4500706C24 /* Sources */ = { 262 | isa = PBXSourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | B6261D961EC94B4500706C24 /* OGPreviewable.swift in Sources */, 266 | B6261D971EC94B4500706C24 /* OGTypeProtcols.swift in Sources */, 267 | B6261D981EC94B4500706C24 /* Parser.swift in Sources */, 268 | B6261D991EC94B4500706C24 /* OGTypes.swift in Sources */, 269 | B6261D9B1EC94B4500706C24 /* TagTracker.swift in Sources */, 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | B69B9A7F1D29F3CC001EBF97 /* Sources */ = { 274 | isa = PBXSourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | B6261D931EC93DDA00706C24 /* OGPreviewable.swift in Sources */, 278 | B69B9AB51D29FD98001EBF97 /* OGTypeProtcols.swift in Sources */, 279 | B69B9AB81D29FD98001EBF97 /* Parser.swift in Sources */, 280 | B69B9AB71D29FD98001EBF97 /* OGTypes.swift in Sources */, 281 | B6DC94E51D2CBDBF00C9A005 /* TagTracker.swift in Sources */, 282 | ); 283 | runOnlyForDeploymentPostprocessing = 0; 284 | }; 285 | B69B9A8A1D29F3CC001EBF97 /* Sources */ = { 286 | isa = PBXSourcesBuildPhase; 287 | buildActionMask = 2147483647; 288 | files = ( 289 | B69B9AB01D29FC72001EBF97 /* Tests.swift in Sources */, 290 | ); 291 | runOnlyForDeploymentPostprocessing = 0; 292 | }; 293 | /* End PBXSourcesBuildPhase section */ 294 | 295 | /* Begin PBXTargetDependency section */ 296 | B69B9A911D29F3CC001EBF97 /* PBXTargetDependency */ = { 297 | isa = PBXTargetDependency; 298 | target = B69B9A831D29F3CC001EBF97 /* OG-iOS */; 299 | targetProxy = B69B9A901D29F3CC001EBF97 /* PBXContainerItemProxy */; 300 | }; 301 | /* End PBXTargetDependency section */ 302 | 303 | /* Begin XCBuildConfiguration section */ 304 | B6261DA11EC94B4500706C24 /* Debug */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ARCHS = "$(ARCHS_STANDARD_32_64_BIT)"; 308 | CLANG_ENABLE_MODULES = YES; 309 | DEFINES_MODULE = YES; 310 | DYLIB_COMPATIBILITY_VERSION = 1; 311 | DYLIB_CURRENT_VERSION = 1; 312 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 313 | INFOPLIST_FILE = Info.plist; 314 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 315 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 316 | MACOSX_DEPLOYMENT_TARGET = 10.10; 317 | PRODUCT_BUNDLE_IDENTIFIER = net.thisismyinter.OG; 318 | PRODUCT_MODULE_NAME = OG; 319 | PRODUCT_NAME = OG; 320 | SDKROOT = macosx; 321 | SKIP_INSTALL = YES; 322 | }; 323 | name = Debug; 324 | }; 325 | B6261DA21EC94B4500706C24 /* Release */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | ARCHS = "$(ARCHS_STANDARD_32_64_BIT)"; 329 | CLANG_ENABLE_MODULES = YES; 330 | DEFINES_MODULE = YES; 331 | DYLIB_COMPATIBILITY_VERSION = 1; 332 | DYLIB_CURRENT_VERSION = 1; 333 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 334 | INFOPLIST_FILE = Info.plist; 335 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 336 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 337 | MACOSX_DEPLOYMENT_TARGET = 10.10; 338 | PRODUCT_BUNDLE_IDENTIFIER = net.thisismyinter.OG; 339 | PRODUCT_MODULE_NAME = OG; 340 | PRODUCT_NAME = OG; 341 | SDKROOT = macosx; 342 | SKIP_INSTALL = YES; 343 | }; 344 | name = Release; 345 | }; 346 | B69B9A961D29F3CC001EBF97 /* Debug */ = { 347 | isa = XCBuildConfiguration; 348 | buildSettings = { 349 | ALWAYS_SEARCH_USER_PATHS = NO; 350 | CLANG_ANALYZER_NONNULL = YES; 351 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 352 | CLANG_CXX_LIBRARY = "libc++"; 353 | CLANG_ENABLE_MODULES = YES; 354 | CLANG_ENABLE_OBJC_ARC = YES; 355 | CLANG_WARN_BOOL_CONVERSION = YES; 356 | CLANG_WARN_CONSTANT_CONVERSION = YES; 357 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 358 | CLANG_WARN_EMPTY_BODY = YES; 359 | CLANG_WARN_ENUM_CONVERSION = YES; 360 | CLANG_WARN_INT_CONVERSION = YES; 361 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 362 | CLANG_WARN_UNREACHABLE_CODE = YES; 363 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 364 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 365 | COPY_PHASE_STRIP = NO; 366 | CURRENT_PROJECT_VERSION = 1; 367 | DEBUG_INFORMATION_FORMAT = dwarf; 368 | ENABLE_STRICT_OBJC_MSGSEND = YES; 369 | ENABLE_TESTABILITY = YES; 370 | GCC_C_LANGUAGE_STANDARD = gnu99; 371 | GCC_DYNAMIC_NO_PIC = NO; 372 | GCC_NO_COMMON_BLOCKS = YES; 373 | GCC_OPTIMIZATION_LEVEL = 0; 374 | GCC_PREPROCESSOR_DEFINITIONS = ( 375 | "DEBUG=1", 376 | "$(inherited)", 377 | ); 378 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 379 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 380 | GCC_WARN_UNDECLARED_SELECTOR = YES; 381 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 382 | GCC_WARN_UNUSED_FUNCTION = YES; 383 | GCC_WARN_UNUSED_VARIABLE = YES; 384 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 385 | ONLY_ACTIVE_ARCH = YES; 386 | SDKROOT = iphoneos; 387 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 388 | SWIFT_VERSION = 5.0; 389 | TARGETED_DEVICE_FAMILY = "1,2"; 390 | VERSIONING_SYSTEM = "apple-generic"; 391 | VERSION_INFO_PREFIX = ""; 392 | }; 393 | name = Debug; 394 | }; 395 | B69B9A971D29F3CC001EBF97 /* Release */ = { 396 | isa = XCBuildConfiguration; 397 | buildSettings = { 398 | ALWAYS_SEARCH_USER_PATHS = NO; 399 | CLANG_ANALYZER_NONNULL = YES; 400 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 401 | CLANG_CXX_LIBRARY = "libc++"; 402 | CLANG_ENABLE_MODULES = YES; 403 | CLANG_ENABLE_OBJC_ARC = YES; 404 | CLANG_WARN_BOOL_CONVERSION = YES; 405 | CLANG_WARN_CONSTANT_CONVERSION = YES; 406 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 407 | CLANG_WARN_EMPTY_BODY = YES; 408 | CLANG_WARN_ENUM_CONVERSION = YES; 409 | CLANG_WARN_INT_CONVERSION = YES; 410 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 411 | CLANG_WARN_UNREACHABLE_CODE = YES; 412 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 413 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 414 | COPY_PHASE_STRIP = NO; 415 | CURRENT_PROJECT_VERSION = 1; 416 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 417 | ENABLE_NS_ASSERTIONS = NO; 418 | ENABLE_STRICT_OBJC_MSGSEND = YES; 419 | GCC_C_LANGUAGE_STANDARD = gnu99; 420 | GCC_NO_COMMON_BLOCKS = YES; 421 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 422 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 423 | GCC_WARN_UNDECLARED_SELECTOR = YES; 424 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 425 | GCC_WARN_UNUSED_FUNCTION = YES; 426 | GCC_WARN_UNUSED_VARIABLE = YES; 427 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 428 | SDKROOT = iphoneos; 429 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 430 | SWIFT_VERSION = 5.0; 431 | TARGETED_DEVICE_FAMILY = "1,2"; 432 | VALIDATE_PRODUCT = YES; 433 | VERSIONING_SYSTEM = "apple-generic"; 434 | VERSION_INFO_PREFIX = ""; 435 | }; 436 | name = Release; 437 | }; 438 | B69B9A991D29F3CC001EBF97 /* Debug */ = { 439 | isa = XCBuildConfiguration; 440 | buildSettings = { 441 | CLANG_ENABLE_MODULES = YES; 442 | DEFINES_MODULE = YES; 443 | DYLIB_COMPATIBILITY_VERSION = 1; 444 | DYLIB_CURRENT_VERSION = 1; 445 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 446 | INFOPLIST_FILE = Info.plist; 447 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 448 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 449 | PRODUCT_BUNDLE_IDENTIFIER = net.thisismyinter.OG; 450 | PRODUCT_MODULE_NAME = OG; 451 | PRODUCT_NAME = OG; 452 | SKIP_INSTALL = YES; 453 | }; 454 | name = Debug; 455 | }; 456 | B69B9A9A1D29F3CC001EBF97 /* Release */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | CLANG_ENABLE_MODULES = YES; 460 | DEFINES_MODULE = YES; 461 | DYLIB_COMPATIBILITY_VERSION = 1; 462 | DYLIB_CURRENT_VERSION = 1; 463 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 464 | INFOPLIST_FILE = Info.plist; 465 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 466 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 467 | PRODUCT_BUNDLE_IDENTIFIER = net.thisismyinter.OG; 468 | PRODUCT_MODULE_NAME = OG; 469 | PRODUCT_NAME = OG; 470 | SKIP_INSTALL = YES; 471 | }; 472 | name = Release; 473 | }; 474 | B69B9A9C1D29F3CC001EBF97 /* Debug */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | CLANG_ENABLE_MODULES = YES; 478 | INFOPLIST_FILE = OGTests/Info.plist; 479 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 480 | PRODUCT_BUNDLE_IDENTIFIER = net.thisismyinter.OGTests; 481 | PRODUCT_NAME = "$(TARGET_NAME)"; 482 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 483 | }; 484 | name = Debug; 485 | }; 486 | B69B9A9D1D29F3CC001EBF97 /* Release */ = { 487 | isa = XCBuildConfiguration; 488 | buildSettings = { 489 | CLANG_ENABLE_MODULES = YES; 490 | INFOPLIST_FILE = OGTests/Info.plist; 491 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 492 | PRODUCT_BUNDLE_IDENTIFIER = net.thisismyinter.OGTests; 493 | PRODUCT_NAME = "$(TARGET_NAME)"; 494 | }; 495 | name = Release; 496 | }; 497 | /* End XCBuildConfiguration section */ 498 | 499 | /* Begin XCConfigurationList section */ 500 | B6261DA01EC94B4500706C24 /* Build configuration list for PBXNativeTarget "OG-macOS" */ = { 501 | isa = XCConfigurationList; 502 | buildConfigurations = ( 503 | B6261DA11EC94B4500706C24 /* Debug */, 504 | B6261DA21EC94B4500706C24 /* Release */, 505 | ); 506 | defaultConfigurationIsVisible = 0; 507 | defaultConfigurationName = Release; 508 | }; 509 | B69B9A7E1D29F3CC001EBF97 /* Build configuration list for PBXProject "OG" */ = { 510 | isa = XCConfigurationList; 511 | buildConfigurations = ( 512 | B69B9A961D29F3CC001EBF97 /* Debug */, 513 | B69B9A971D29F3CC001EBF97 /* Release */, 514 | ); 515 | defaultConfigurationIsVisible = 0; 516 | defaultConfigurationName = Release; 517 | }; 518 | B69B9A981D29F3CC001EBF97 /* Build configuration list for PBXNativeTarget "OG-iOS" */ = { 519 | isa = XCConfigurationList; 520 | buildConfigurations = ( 521 | B69B9A991D29F3CC001EBF97 /* Debug */, 522 | B69B9A9A1D29F3CC001EBF97 /* Release */, 523 | ); 524 | defaultConfigurationIsVisible = 0; 525 | defaultConfigurationName = Release; 526 | }; 527 | B69B9A9B1D29F3CC001EBF97 /* Build configuration list for PBXNativeTarget "OGTests" */ = { 528 | isa = XCConfigurationList; 529 | buildConfigurations = ( 530 | B69B9A9C1D29F3CC001EBF97 /* Debug */, 531 | B69B9A9D1D29F3CC001EBF97 /* Release */, 532 | ); 533 | defaultConfigurationIsVisible = 0; 534 | defaultConfigurationName = Release; 535 | }; 536 | /* End XCConfigurationList section */ 537 | }; 538 | rootObject = B69B9A7B1D29F3CC001EBF97 /* Project object */; 539 | } 540 | --------------------------------------------------------------------------------