├── Example ├── README.md ├── SwiftLinkPreviewExample │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── Placeholder.imageset │ │ │ ├── no image.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AlamofireSource.swift │ ├── Info.plist │ ├── Storyboards │ │ └── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ ├── Delegates │ │ └── AppDelegate.swift │ └── Controllers │ │ └── ViewController.swift ├── Podfile └── SwiftLinkPreviewExample.xcodeproj │ ├── xcshareddata │ └── xcschemes │ │ └── SwiftLinkPreviewExample.xcscheme │ └── project.pbxproj ├── SwiftLinkPreviewTests ├── SwiftLinkPreviewTests-Bridging-Header.h ├── Files │ ├── head-meta-icon.html │ ├── head-title.html │ ├── body-image-single.html │ ├── head-meta-base.html │ ├── body-text-p.html │ ├── body-text-span.html │ ├── body-text-div.html │ ├── body-image-gallery.html │ ├── head-meta-meta.html │ ├── head-meta-itemprop.html │ ├── head-meta-facebook.html │ └── head-meta-twitter.html ├── Utils │ ├── IntExtension.swift │ ├── File.swift │ └── StringTestExtension.swift ├── Info │ ├── Info.plist │ ├── Info-tvOS.plist │ └── Info-macOS.plist ├── RegexTests.swift ├── VideoTests.swift ├── BaseURLTests.swift ├── IconTests.swift ├── TitleTests.swift ├── Constants │ ├── Constants.swift │ └── URLs.swift ├── HugeTests.swift ├── ImageTests.swift ├── BodyTests.swift └── MetaTests.swift ├── Images ├── badge.png ├── images.gif ├── langs.gif ├── videos.gif ├── default.gif └── gallery.gif ├── Gemfile ├── Dangerfile ├── .github ├── PULL_REQUEST_TEMPLATE.md └── CONTRIBUTING.md ├── Headers └── SwiftLinkPreview.h ├── SwiftLinkPreview.podspec ├── Package.swift ├── Sources ├── Classes │ ├── Response.swift │ ├── PreviewError.swift │ ├── Cache.swift │ └── Regex.swift ├── Info │ ├── Info.plist │ ├── Info-tvOS.plist │ ├── Info-watchOS.plist │ └── Info-macOS.plist └── Extensions │ ├── NSURLSessionExtension.swift │ ├── ResponseExtension.swift │ └── StringExtension.swift ├── tests.sh ├── LICENSE ├── .travis.yml ├── .gitignore ├── SwiftLinkPreview.xcodeproj └── xcshareddata │ └── xcschemes │ ├── SwiftLinkPreviewWatchOS.xcscheme │ ├── SwiftLinkPreviewTvOS.xcscheme │ ├── SwiftLinkPreviewMacOS.xcscheme │ └── SwiftLinkPreview.xcscheme ├── README.md └── CHANGELOG.md /Example/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | Run `pod install` 4 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/SwiftLinkPreviewTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Images/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonardoCardoso/SwiftLinkPreview/HEAD/Images/badge.png -------------------------------------------------------------------------------- /Images/images.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonardoCardoso/SwiftLinkPreview/HEAD/Images/images.gif -------------------------------------------------------------------------------- /Images/langs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonardoCardoso/SwiftLinkPreview/HEAD/Images/langs.gif -------------------------------------------------------------------------------- /Images/videos.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonardoCardoso/SwiftLinkPreview/HEAD/Images/videos.gif -------------------------------------------------------------------------------- /Images/default.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonardoCardoso/SwiftLinkPreview/HEAD/Images/default.gif -------------------------------------------------------------------------------- /Images/gallery.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonardoCardoso/SwiftLinkPreview/HEAD/Images/gallery.gif -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # A sample Gemfile 3 | source "https://rubygems.org" 4 | 5 | # gem "rails" 6 | gem 'danger' -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/head-meta-icon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | [:body-random] 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample/Assets.xcassets/Placeholder.imageset/no image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonardoCardoso/SwiftLinkPreview/HEAD/Example/SwiftLinkPreviewExample/Assets.xcassets/Placeholder.imageset/no image.png -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/head-title.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random-pre] 4 | [:title] 5 | [:head-random-pos] 6 | 7 | 8 | [:body-random] 9 | 10 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/body-image-single.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random] 4 | 5 | 6 | [:body-random-pre] 7 | 8 | [:body-random-pos] 9 | 10 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # Add a CHANGELOG entry for app changes 2 | if !git.modified_files.include?("CHANGELOG.md") 3 | fail("Please include a CHANGELOG entry. \nYou can find it at [CHANGELOG.md](https://github.com/LeonardoCardoso/SwiftLinkPreview/blob/master/CHANGELOG.md).") 4 | end -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/head-meta-base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random] 4 | 5 | 6 | 7 | [:body-random] 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/body-text-p.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random] 4 | 5 | 6 | [:body-random-pre] 7 |

[:random-1]

8 | [:body-random-middle] 9 |

[:random-2]

10 | [:body-random-pos] 11 | 12 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/body-text-span.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random] 4 | 5 | 6 | [:body-random-pre] 7 | [:random-1] 8 | [:body-random-middle] 9 | [:random-2] 10 | [:body-random-pos] 11 | 12 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/body-text-div.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random] 4 | 5 | 6 | [:body-random-pre] 7 |
[:random-1]
8 | [:body-random-middle] 9 |
[:random-2]
10 | [:body-random-pos] 11 | 12 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/body-image-gallery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random] 4 | 5 | 6 | 7 | [:body-random-pre] 8 | 9 | [:body-random-middle] 10 | 11 | [:body-random-pos] 12 | 13 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/head-meta-meta.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random-pre] 4 | 5 | 6 | 7 | 8 | [:head-random-pos] 9 | 10 | 11 | [:body-random] 12 | 13 | 14 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/head-meta-itemprop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random-pre] 4 | 5 | 6 | 7 | 8 | [:head-random-pos] 9 | 10 | 11 | [:body-random] 12 | 13 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Utils/IntExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntExtension.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 16/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Int { 12 | static func random(_ lower: Int = 0, upper: Int = 100) -> Int { 13 | return lower + Int(arc4random_uniform(UInt32(upper - 1 - lower + 1))) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample/Assets.xcassets/Placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "no image.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/head-meta-facebook.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random-pre] 4 | 5 | 6 | 7 | 8 | [:head-random-pos] 9 | 10 | 11 | [:body-random] 12 | 13 | 14 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Files/head-meta-twitter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [:head-random-pre] 4 | 5 | 6 | 7 | 8 | [:head-random-pos] 9 | 10 | 11 | [:body-random] 12 | 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #### Action 7 | 8 | 9 | - Add a brief description of what was made 10 | - Issues: #issue-number #issue-number ... 11 | - Commits: #commit-hash ... 12 | - ... -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Utils/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 05/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class File { 12 | // Read local html files 13 | static func toString(_ file: String) -> String { 14 | let path = Bundle(for: object_getClass(self)!).path(forResource: file, ofType: "html") 15 | let fileHtml = try! NSString(contentsOfFile: path!, encoding: String.Encoding.utf8.rawValue) 16 | return String(fileHtml) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Headers/SwiftLinkPreview.h: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLinkPreview.h 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 09/06/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | //! Project version number for SwiftLinkPreview. 12 | FOUNDATION_EXPORT double SwiftLinkPreviewVersionNumber; 13 | 14 | //! Project version string for SwiftLinkPreview. 15 | FOUNDATION_EXPORT const unsigned char SwiftLinkPreviewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | -------------------------------------------------------------------------------- /SwiftLinkPreview.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.ios.deployment_target = '8.0' 4 | s.osx.deployment_target = "10.10" 5 | s.watchos.deployment_target = '2.0' 6 | s.tvos.deployment_target = '9.0' 7 | s.name = "SwiftLinkPreview" 8 | s.summary = "It makes a preview from an url, grabbing all the information such as title, relevant texts and images." 9 | s.requires_arc = true 10 | s.version = "4.0.0" 11 | s.license = { :type => "MIT", :file => "LICENSE" } 12 | s.author = { "Leonardo Cardoso" => "contact@leocardz.com" } 13 | s.homepage = "https://github.com/LeonardoCardoso/SwiftLinkPreview" 14 | s.source = { :git => "https://github.com/LeonardoCardoso/SwiftLinkPreview.git", :tag => s.version } 15 | s.source_files = "Sources/**/*.swift" 16 | s.swift_version = '5' 17 | 18 | end 19 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // 3 | // Package.swift 4 | // SwiftLinkPreview 5 | // 6 | // Created by Leonardo Cardoso on 04/07/2016. 7 | // Copyright © 2016 leocardz.com. All rights reserved. 8 | // 9 | 10 | import PackageDescription 11 | 12 | let package = Package( 13 | name: "SwiftLinkPreview", 14 | platforms: [ 15 | .iOS("8.0"), 16 | .macOS("10.11"), 17 | .tvOS("9.0"), 18 | .watchOS("2.0") 19 | ], 20 | products: [ 21 | .library( 22 | name: "SwiftLinkPreview", 23 | targets: ["SwiftLinkPreview"] 24 | ), 25 | ], 26 | targets: [ 27 | .target( 28 | name: "SwiftLinkPreview", 29 | dependencies: [], 30 | path: "Sources" 31 | ), 32 | ], 33 | swiftLanguageVersions: [.v5] 34 | ) 35 | -------------------------------------------------------------------------------- /Sources/Classes/Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Giuseppe Travasoni on 20/11/2018. 6 | // Copyright © 2018 leocardz.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Response: Sendable { 12 | public internal(set) var baseURL: String? 13 | public internal(set) var url: URL? 14 | public internal(set) var finalUrl: URL? 15 | public internal(set) var canonicalUrl: String? 16 | public internal(set) var title: String? 17 | public internal(set) var description: String? 18 | public internal(set) var images: [String]? 19 | public internal(set) var image: String? 20 | public internal(set) var icon: String? 21 | public internal(set) var video: String? 22 | public internal(set) var price: String? 23 | 24 | public init() { } 25 | } 26 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Info/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 | 3.5.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Info/Info-tvOS.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 | 3.5.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Info/Info-macOS.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 | 3.5.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Info/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 | 3.5.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Info/Info-tvOS.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 | 3.5.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Info/Info-watchOS.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 | 3.5.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '13.0' 3 | 4 | target 'SwiftLinkPreviewExample' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for SwiftLinkPreviewExample 9 | pod 'Alamofire', '5.4.1' 10 | pod 'SwiftyDrop', '4.2.0' 11 | pod 'ImageSlideshow', '1.9.1' 12 | pod 'ImageSlideshow/Alamofire', '1.9.1' 13 | pod 'SwiftLinkPreview', :path => '../' 14 | 15 | end 16 | 17 | post_install do |installer| 18 | installer.pods_project.targets.each do |target| 19 | target.build_configurations.each do |config| 20 | if ['SwiftyDrop'].include?(target.name) 21 | config.build_settings['SWIFT_VERSION'] = '4.2' 22 | else 23 | config.build_settings['SWIFT_VERSION'] = '5.0' 24 | end 25 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0' 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/RegexTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegexTests.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 16/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | @testable import SwiftLinkPreview 10 | import XCTest 11 | 12 | // This class tests URLs 13 | final class RegexTests: XCTestCase { 14 | // MARK: - Vars 15 | 16 | let slp = SwiftLinkPreview() 17 | 18 | // MARK: - Functions 19 | 20 | func testURL() { 21 | for url in URLs.bunch { 22 | let extracted = slp.extractURL(text: url[0]) 23 | 24 | XCTAssertEqual(extracted?.absoluteString, url[1]) 25 | } 26 | } 27 | 28 | func testCanonicalURL() throws { 29 | for url in URLs.bunch { 30 | let finalUrl = try XCTUnwrap(URL(string: url[1])) 31 | 32 | let canonical = slp.extractCanonicalURL(finalUrl) 33 | 34 | XCTAssertEqual(canonical, url[2]) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Info/Info-macOS.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 | 3.5.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2016 leocardz.com. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/VideoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoTests.swift 3 | // SwiftLinkPreviewTests 4 | // 5 | // Created by Jeff Hodsdon on 4/15/21. 6 | // Copyright © 2021 leocardz.com. All rights reserved. 7 | // 8 | 9 | @testable import SwiftLinkPreview 10 | import XCTest 11 | 12 | // This final class tests videos 13 | final class VideoTests: XCTestCase { 14 | // MARK: - Vars 15 | 16 | let slp = SwiftLinkPreview() 17 | 18 | func testImgur() throws { 19 | let url = try XCTUnwrap(URL(string: "https://imgur.com/GaI4Ruu")) 20 | let source = try String(contentsOf: url).extendedTrim 21 | 22 | let result = slp.crawlMetaTags(source, result: Response()) 23 | 24 | XCTAssert(result.video != nil) 25 | } 26 | 27 | func testGiphy() throws { 28 | let url = try XCTUnwrap(URL(string: "https://giphy.com/gifs/cuddles-yPQcB2bQVBQ6k")) 29 | let source = try String(contentsOf: url).extendedTrim 30 | 31 | let result = slp.crawlMetaTags(source, result: Response()) 32 | 33 | XCTAssert(result.video != nil) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests.sh: -------------------------------------------------------------------------------- 1 | xcodebuild -project SwiftLinkPreview.xcodeproj -scheme SwiftLinkPreview -destination "OS=10.0,name=iPhone 7" -sdk "iphonesimulator10.0" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty -c; 2 | xcodebuild -project SwiftLinkPreview.xcodeproj -scheme SwiftLinkPreviewWatchOS -destination "OS=3.0,name=Apple Watch Series 2 - 42mm" -sdk "watchsimulator3.0" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty -c; 3 | xcodebuild -project SwiftLinkPreview.xcodeproj -scheme SwiftLinkPreviewTvOS -destination "OS=10.0,name=Apple TV 1080p" -sdk "appletvsimulator10.0" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty -c; 4 | xcodebuild -project SwiftLinkPreview.xcodeproj -scheme SwiftLinkPreviewMacOS -destination "arch=x86_64" -sdk "macosx10.12" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty -c; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Leonardo Cardoso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample/AlamofireSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlamofireSource.swift 3 | // Pods 4 | // 5 | // Created by Petr Zvoníček on 14.01.16. 6 | // 7 | // This class is for the Example only 8 | 9 | import Alamofire 10 | import AlamofireImage 11 | import ImageSlideshow 12 | 13 | public class AlamofireSource: NSObject, InputSource { 14 | var url: NSURL? 15 | 16 | public init(url: NSURL) { 17 | self.url = url 18 | super.init() 19 | } 20 | 21 | public init?(urlString: String) { 22 | if let validUrl = NSURL(string: urlString) { 23 | self.url = validUrl 24 | super.init() 25 | } else { 26 | super.init() 27 | return nil 28 | } 29 | } 30 | 31 | public func load(to imageView: UIImageView, with callback: @escaping (UIImage?) -> Void) { 32 | guard let url = url as URL? else { return } 33 | imageView.af.setImage( 34 | withURL: url, 35 | placeholderImage: nil, 36 | filter: nil, 37 | progress: nil 38 | ) { response in 39 | let result = try? response.result.get() 40 | imageView.image = result 41 | callback(result) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Classes/PreviewError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewError.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 09/06/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | public enum PreviewError: Error, Sendable, CustomStringConvertible { 11 | case noURLHasBeenFound(String?) 12 | case invalidURL(String?) 13 | case cannotBeOpened(String?) 14 | case parseError(String?) 15 | 16 | public var description: String { 17 | switch self { 18 | case let .noURLHasBeenFound(error): 19 | NSLocalizedString("No URL has been found. \(reason(error))", comment: String()) 20 | case let .invalidURL(error): 21 | NSLocalizedString("This data is not valid URL. \(reason(error)).", comment: String()) 22 | case let .cannotBeOpened(error): 23 | NSLocalizedString("This URL cannot be opened. \(reason(error)).", comment: String()) 24 | case let .parseError(error): 25 | NSLocalizedString("An error occurred when parsing the HTML. \(reason(error)).", comment: String()) 26 | } 27 | } 28 | 29 | private func reason(_ error: String?) -> String { 30 | "Reason: \(error ?? String())" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Extensions/NSURLSessionExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSURLSessionExtension.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 15/06/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | public extension URLSession { 11 | func synchronousDataTask(with url: URL) -> (Data?, URLResponse?, NSError?) { 12 | var data: Data?, response: URLResponse?, error: NSError? 13 | let semaphore = DispatchSemaphore(value: 0) 14 | 15 | dataTask(with: url, completionHandler: { 16 | data = $0; response = $1; error = $2 as NSError? 17 | semaphore.signal() 18 | }).resume() 19 | 20 | _ = semaphore.wait(timeout: DispatchTime.distantFuture) 21 | 22 | return (data, response, error) 23 | } 24 | 25 | func synchronousDataTask(with request: URLRequest) -> (Data?, URLResponse?, NSError?) { 26 | var data: Data?, response: URLResponse?, error: NSError? 27 | let semaphore = DispatchSemaphore(value: 0) 28 | 29 | dataTask(with: request, completionHandler: { 30 | data = $0; response = $1; error = $2 as NSError? 31 | semaphore.signal() 32 | }).resume() 33 | 34 | _ = semaphore.wait(timeout: DispatchTime.distantFuture) 35 | 36 | return (data, response, error) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10 3 | env: 4 | global: 5 | - LC_CTYPE=en_US.UTF-8 6 | - LANG=en_US.UTF-8 7 | - PROJECT=SwiftLinkPreview.xcodeproj 8 | script: 9 | - xcodebuild -project SwiftLinkPreview.xcodeproj -scheme SwiftLinkPreview -destination "OS=12.0,name=iPhone 7" -sdk "iphonesimulator12.0" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty -c; 10 | - xcodebuild -project SwiftLinkPreview.xcodeproj -scheme SwiftLinkPreviewWatchOS -destination "OS=5.0,name=Apple Watch Series 4 - 44mm" -sdk "watchsimulator5.0" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty -c; 11 | - xcodebuild -project SwiftLinkPreview.xcodeproj -scheme SwiftLinkPreviewTvOS -destination "OS=12.0,name=Apple TV 1080p" -sdk "appletvsimulator12.0" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty -c; 12 | - xcodebuild -project SwiftLinkPreview.xcodeproj -scheme SwiftLinkPreviewMacOS -destination "arch=x86_64" -sdk "macosx10.14" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty -c; 13 | - bundle exec danger; 14 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/BaseURLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseURLTests.swift 3 | // SwiftLinkPreviewTests 4 | // 5 | // Created by Leonardo Cardoso on 23.09.21. 6 | // Copyright © 2021 leocardz.com. All rights reserved. 7 | // 8 | 9 | @testable import SwiftLinkPreview 10 | import XCTest 11 | 12 | // This class tests head meta info 13 | final class BaseURLTests: XCTestCase { 14 | // MARK: - Vars 15 | 16 | var baseTemplate = "" 17 | let slp = SwiftLinkPreview() 18 | 19 | // MARK: - SetUps 20 | 21 | // Those setup functions get that template, and fulfil determinated areas with rand texts, images and tags 22 | override func setUp() { 23 | super.setUp() 24 | 25 | baseTemplate = File.toString(Constants.headMetaBase) 26 | } 27 | 28 | // MARK: - Base 29 | 30 | func setUpBaseAndRun() { 31 | var baseTemplate = self.baseTemplate 32 | baseTemplate = baseTemplate.replace(Constants.headRandom, with: String.randomTag()) 33 | baseTemplate = baseTemplate.replace(Constants.bodyRandom, with: String.randomTag()).extendedTrim 34 | 35 | let result = slp.crawlMetaBase(baseTemplate, result: Response()) 36 | 37 | XCTAssertEqual(result.baseURL, "https://host/resource/index/") 38 | } 39 | 40 | func testBase() { 41 | for _ in 0 ..< 100 { 42 | setUpBaseAndRun() 43 | } 44 | } 45 | 46 | func testResultBase() { 47 | XCTAssertEqual( 48 | slp.formatImageURLs(["assets/test.png"], base: "https://host/resource/index/")?.first, 49 | "https://host/resource/index/assets/test.png" 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 3.5.0 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIMainStoryboardFile 33 | Main 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UIRequiresFullScreen 39 | 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | *.DS_Store 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xcuserstate 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | Pods/ 49 | Podfile.lock* 50 | *.xcworkspace 51 | SwiftLinkPreview/*/*.podspec 52 | 53 | # Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | Carthage/Checkouts 57 | 58 | Carthage/Build 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 63 | # screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots 70 | fastlane/test_output 71 | Gemfile.lock 72 | .swiftpm 73 | -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample/Storyboards/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/IconTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconTests.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Vincent Toms on 7/21/17. 6 | // Copyright © 2017 leocardz.com. All rights reserved. 7 | // 8 | 9 | @testable import SwiftLinkPreview 10 | import XCTest 11 | 12 | final class IconTests: XCTestCase { 13 | let slp = SwiftLinkPreview() 14 | 15 | var template = "" 16 | 17 | let iconList = [ 18 | "/apple-touch-icon.png", 19 | "/touch-icon-ipad.png", 20 | "/touch-icon-iphone4.png", 21 | "http://github.com/images/touch-icon-iphone4.png", 22 | "http://github.com/images/touch-icon-ipad.png", 23 | "http://github.com/images/apple-touch-icon-57x57.png", 24 | "/favicon.ico", 25 | "/fluid-icon.png", 26 | ] 27 | 28 | let typeList = [ 29 | "apple-touch-icon", 30 | "apple-touch-icon-precomposed", 31 | "shortcut icon", 32 | "fluid-icon", 33 | ] 34 | 35 | override func setUp() { 36 | super.setUp() 37 | 38 | template = File.toString(Constants.bodyIcon) 39 | } 40 | 41 | func testLink() { 42 | for _ in 1 ..< 1000 { 43 | let icon = random(array: iconList) 44 | let type = random(array: typeList) 45 | var testTemplate = template 46 | 47 | testTemplate = testTemplate.replace(Constants.href, with: icon) 48 | testTemplate = testTemplate.replace(Constants.rel, with: type) 49 | 50 | var result = Response() 51 | result.url = URL(string: "google.com") 52 | result.canonicalUrl = "google.com" 53 | result.finalUrl = URL(string: "https://google.com") 54 | 55 | result = slp.crawIcon(testTemplate, result: result) 56 | 57 | let url = icon.range(of: "http") != nil ? icon : "https://google.com/\(icon)".replace("com//", with: "com/") 58 | 59 | XCTAssertEqual(url, result.icon) 60 | } 61 | } 62 | 63 | fileprivate func random(array: [String]) -> String { 64 | let randomIndex = Int(arc4random_uniform(UInt32(array.count))) 65 | return array[randomIndex] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/TitleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleTests.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 05/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | @testable import SwiftLinkPreview 10 | import XCTest 11 | 12 | // This class tests head title 13 | final class TitleTests: XCTestCase { 14 | // MARK: - Vars 15 | 16 | var titleTemplate = "" 17 | let slp = SwiftLinkPreview() 18 | 19 | // MARK: - SetUps 20 | 21 | // Those setup functions get that template, and fulfil determinated areas with rand texts, images and tags 22 | override func setUp() { 23 | super.setUp() 24 | 25 | titleTemplate = File.toString(Constants.headTitle) 26 | } 27 | 28 | // MARK: - Title 29 | 30 | func setUpTitle() throws { 31 | let metaData = 32 | [ 33 | Constants.title: String.randomText(), 34 | Constants.headRandom: String.randomTag(), 35 | Constants.bodyRandom: String.randomTag(), 36 | ] 37 | 38 | var metaTemplate = titleTemplate 39 | let title = try XCTUnwrap(metaData[Constants.title]) 40 | let headRandom = try XCTUnwrap(metaData[Constants.headRandom]) 41 | let bodyRandom = try XCTUnwrap(metaData[Constants.bodyRandom]) 42 | metaTemplate = metaTemplate.replace(Constants.headRandomPre, with: headRandom) 43 | metaTemplate = metaTemplate.replace(Constants.headRandomPos, with: headRandom) 44 | 45 | metaTemplate = metaTemplate.replace(Constants.title, with: title) 46 | 47 | metaTemplate = metaTemplate.replace(Constants.bodyRandom, with: bodyRandom).extendedTrim 48 | 49 | let response = slp.crawlTitle(metaTemplate, result: Response()) 50 | 51 | let comparable = response.result.title 52 | let comparison = comparable == title.decoded.extendedTrim || 53 | comparable == headRandom.decoded.extendedTrim || 54 | comparable == bodyRandom.decoded.extendedTrim 55 | 56 | XCTAssert(comparison) 57 | } 58 | 59 | func testTitle() throws { 60 | for _ in 0 ..< 100 { 61 | try setUpTitle() 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Constants/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 05/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Constants { 12 | static let huge = "huge" 13 | static let bodyTitle = "body-text-span" 14 | static let bodyTextSpan = "body-text-span" 15 | static let bodyTextP = "body-text-p" 16 | static let bodyTextDiv = "body-text-div" 17 | static let bodyImageSingle = "body-image-single" 18 | static let bodyImageGallery = "body-image-gallery" 19 | static let bodyIcon = "head-meta-icon" 20 | static let headMetaTwitter = "head-meta-twitter" 21 | static let headMetaMeta = "head-meta-meta" 22 | static let headMetaBase = "head-meta-base" 23 | static let headMetaItemprop = "head-meta-itemprop" 24 | static let headMetaFacebook = "head-meta-facebook" 25 | static let headTitle = "head-title" 26 | 27 | static let headRandom = "[:head-random]" 28 | static let headRandomPre = "[:head-random-pre]" 29 | static let headRandomPos = "[:head-random-pos]" 30 | 31 | static let bodyRandom = "[:body-random]" 32 | static let bodyRandomPre = "[:body-random-pre]" 33 | static let bodyRandomMiddle = "[:body-random-middle]" 34 | static let bodyRandomPos = "[:body-random-pos]" 35 | 36 | static let twitterTitle = "[:twitter-title]" 37 | static let twitterSite = "[:twitter-site]" 38 | static let twitterImageSrc = "[:twitter-image-src]" 39 | static let twitterDescription = "[:twitter-description]" 40 | 41 | static let facebookTitle = "[:og-title]" 42 | static let facebookSite = "[:og-url]" 43 | static let facebookImage = "[:og-image]" 44 | static let facebookDescription = "[:og-description]" 45 | 46 | static let title = "[:title]" 47 | static let site = "[:site]" 48 | static let image = "[:image]" 49 | static let description = "[:description]" 50 | 51 | static let image1 = "[:image-1]" 52 | static let image2 = "[:image-2]" 53 | static let image3 = "[:image-3]" 54 | 55 | static let random1 = "[:random-1]" 56 | static let random2 = "[:random-2]" 57 | static let random3 = "[:random-3]" 58 | 59 | static let tag1 = "[:tag-1]" 60 | static let tag2 = "[:tag-2]" 61 | static let tag3 = "[:tag-3]" 62 | 63 | static let href = "[:href]" 64 | static let rel = "[:rel]" 65 | } 66 | -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample/Delegates/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftLinkPreviewExample 4 | // 5 | // Created by Leonardo Cardoso on 09/06/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | func application( 16 | _ application: UIApplication, 17 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 18 | ) -> Bool { 19 | // Override point for customization after application launch. 20 | return true 21 | } 22 | 23 | func applicationWillResignActive(_ application: UIApplication) { 24 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of 25 | // temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the 26 | // application and it begins the transition to the background state. 27 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games 28 | // should use this method to pause the game. 29 | } 30 | 31 | func applicationDidEnterBackground(_ application: UIApplication) { 32 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application 33 | // state information to restore your application to its current state in case it is terminated later. 34 | // If your application supports background execution, this method is called instead of applicationWillTerminate: 35 | // when the user quits. 36 | } 37 | 38 | func applicationWillEnterForeground(_ application: UIApplication) { 39 | // Called as part of the transition from the background to the active state; here you can undo many of the 40 | // changes made on entering the background. 41 | } 42 | 43 | func applicationDidBecomeActive(_ application: UIApplication) { 44 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the 45 | // application was previously in the background, optionally refresh the user interface. 46 | } 47 | 48 | func applicationWillTerminate(_ application: UIApplication) { 49 | // Called when the application is about to terminate. Save data if appropriate. See also 50 | // applicationDidEnterBackground:. 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | This document contains information and guidelines about contributing to this project. 4 | Please read it before you start participating. 5 | 6 | **Topics** 7 | 8 | * [Reporting Issues](#reporting-other-issues) 9 | * [Developers Certificate of Origin](#developers-certificate-of-origin) 10 | 11 | ## Reporting Other Issues 12 | 13 | A great way to contribute to the project 14 | is to send a detailed issue when you encounter an problem. 15 | We always appreciate a well-written, thorough bug report. 16 | 17 | Check that the project issues database 18 | doesn't already include that problem or suggestion before submitting an issue. 19 | If you find a match, add a quick "+1" or "I have this problem too." 20 | Doing this helps prioritize the most common problems and requests. 21 | 22 | When reporting issues, please include the following: 23 | 24 | * The version of Xcode you're using 25 | * The version of iOS or OS X you're targeting 26 | * The full output of any stack trace or compiler error 27 | * A code snippet that reproduces the described behavior, if applicable 28 | * Any other details that would be useful in understanding the problem 29 | 30 | This information will help us review and fix your issue faster. 31 | 32 | ## Developer's Certificate of Origin 1.1 33 | 34 | By making a contribution to this project, I certify that: 35 | 36 | - (a) The contribution was created in whole or in part by me and I 37 | have the right to submit it under the open source license 38 | indicated in the file; or 39 | 40 | - (b) The contribution is based upon previous work that, to the best 41 | of my knowledge, is covered under an appropriate open source 42 | license and I have the right under that license to submit that 43 | work with modifications, whether created in whole or in part 44 | by me, under the same open source license (unless I am 45 | permitted to submit under a different license), as indicated 46 | in the file; or 47 | 48 | - (c) The contribution was provided directly to me by some other 49 | person who certified (a), (b) or (c) and I have not modified 50 | it. 51 | 52 | - (d) I understand and agree that this project and the contribution 53 | are public and that a record of the contribution (including all 54 | personal information I submit with it, including my sign-off) is 55 | maintained indefinitely and may be redistributed consistent with 56 | this project or the open source license(s) involved. 57 | 58 | --- 59 | 60 | *Some of the ideas and wording for the statements above were based on work by the [Alamofire](https://github.com/Alamofire/Alamofire/blob/master/CONTRIBUTING.md) and [Linux](http://elinux.org/Developer_Certificate_Of_Origin) communities. We commend them for their efforts to facilitate collaboration in their projects.* -------------------------------------------------------------------------------- /Sources/Classes/Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cache.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Yehor Popovych on 1/17/17. 6 | // Copyright © 2017 leocardz.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Cache { 12 | func slp_getCachedResponse(url: String) -> Response? 13 | 14 | func slp_setCachedResponse(url: String, response: Response?) 15 | } 16 | 17 | public final class DisabledCache: Cache { 18 | public static let instance = DisabledCache() 19 | 20 | public func slp_getCachedResponse(url: String) -> Response? { nil } 21 | 22 | public func slp_setCachedResponse(url: String, response: Response?) { } 23 | } 24 | 25 | open class InMemoryCache: Cache { 26 | private var cache = [String: (response: Response, date: Date)]() 27 | private let invalidationTimeout: TimeInterval 28 | private let cleanupTimer: DispatchSource? 29 | 30 | // High priority queue for quick responses 31 | private static let cacheQueue = DispatchQueue( 32 | label: "SwiftLinkPreviewInMemoryCacheQueue", 33 | qos: .userInitiated, 34 | target: DispatchQueue.global(qos: .userInitiated) 35 | ) 36 | 37 | public init(invalidationTimeout: TimeInterval = 300.0, cleanupInterval: TimeInterval = 10.0) { 38 | self.invalidationTimeout = invalidationTimeout 39 | 40 | self.cleanupTimer = DispatchSource.makeTimerSource(queue: Self.cacheQueue) as? DispatchSource 41 | cleanupTimer?.schedule(deadline: .now() + cleanupInterval, repeating: cleanupInterval) 42 | 43 | cleanupTimer?.setEventHandler { [weak self] in 44 | guard let self else { return } 45 | self.cleanup() 46 | } 47 | 48 | cleanupTimer?.resume() 49 | } 50 | 51 | open func cleanup() { 52 | Self.cacheQueue.async { [weak self] in 53 | guard let self else { return } 54 | for (url, data) in self.cache { 55 | if data.date.timeIntervalSinceNow >= self.invalidationTimeout { 56 | self.cache[url] = nil 57 | } 58 | } 59 | } 60 | } 61 | 62 | open func slp_getCachedResponse(url: String) -> Response? { 63 | return Self.cacheQueue.sync { [weak self] in 64 | guard let self, let response = cache[url] else { return nil } 65 | 66 | if response.date.timeIntervalSinceNow >= invalidationTimeout { 67 | slp_setCachedResponse(url: url, response: nil) 68 | return nil 69 | } 70 | return response.response 71 | } 72 | } 73 | 74 | open func slp_setCachedResponse(url: String, response: Response?) { 75 | Self.cacheQueue.sync { [weak self] in 76 | guard let self else { return } 77 | if let response = response { 78 | cache[url] = (response, Date()) 79 | } else { 80 | cache[url] = nil 81 | } 82 | } 83 | } 84 | 85 | deinit { 86 | self.cleanupTimer?.cancel() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Extensions/ResponseExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResponseExtension.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Giuseppe Travasoni on 20/11/2018. 6 | // Copyright © 2018 leocardz.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Response { 12 | var dictionary: [String: Any] { 13 | var responseData: [String: Any] = [:] 14 | responseData["baseURL"] = baseURL 15 | responseData["url"] = url 16 | responseData["finalUrl"] = finalUrl 17 | responseData["canonicalUrl"] = canonicalUrl 18 | responseData["title"] = title 19 | responseData["description"] = description 20 | responseData["images"] = images 21 | responseData["image"] = image 22 | responseData["icon"] = icon 23 | responseData["video"] = video 24 | responseData["price"] = price 25 | return responseData 26 | } 27 | 28 | enum Key: String { 29 | case url 30 | case finalUrl 31 | case canonicalUrl 32 | case title 33 | case description 34 | case image 35 | case images 36 | case icon 37 | case video 38 | case baseURL 39 | case price 40 | } 41 | 42 | mutating func set(_ value: Any, for key: Key) { 43 | switch key { 44 | case Key.baseURL: 45 | if let value = value as? String { baseURL = value } 46 | case Key.url: 47 | if let value = value as? URL { url = value } 48 | case Key.finalUrl: 49 | if let value = value as? URL { finalUrl = value } 50 | case Key.canonicalUrl: 51 | if let value = value as? String { canonicalUrl = value } 52 | case Key.title: 53 | if let value = value as? String { title = value } 54 | case Key.description: 55 | if let value = value as? String { description = value } 56 | case Key.image: 57 | if let value = value as? String { image = value } 58 | case Key.images: 59 | if let value = value as? [String] { images = value } 60 | case Key.icon: 61 | if let value = value as? String { icon = value } 62 | case Key.video: 63 | if let value = value as? String { video = value } 64 | case Key.price: 65 | if let value = value as? String { price = value } 66 | } 67 | } 68 | 69 | func value(for key: Key) -> Any? { 70 | switch key { 71 | case Key.baseURL: 72 | return baseURL 73 | case Key.url: 74 | return url 75 | case Key.finalUrl: 76 | return finalUrl 77 | case Key.canonicalUrl: 78 | return canonicalUrl 79 | case Key.title: 80 | return title 81 | case Key.description: 82 | return description 83 | case Key.image: 84 | return image 85 | case Key.images: 86 | return images 87 | case Key.icon: 88 | return icon 89 | case Key.video: 90 | return video 91 | case Key.price: 92 | return price 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/HugeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HugeTests.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 19/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | @testable import SwiftLinkPreview 10 | import XCTest 11 | 12 | // This class tests head meta info 13 | final class HugeTests: XCTestCase { 14 | // MARK: - Vars 15 | 16 | let slp = SwiftLinkPreview() 17 | 18 | // MARK: - Huge 19 | 20 | func testHuge() throws { 21 | do { 22 | // Get reddit.com because it contains a huge HTML 23 | let source = try String(contentsOf: try XCTUnwrap(URL(string: "https://reddit.com"))).extendedTrim 24 | 25 | let title = slp.crawlCode(source, minimum: SwiftLinkPreview.titleMinimumRelevant) 26 | let description = slp.crawlCode(source, minimum: SwiftLinkPreview.decriptionMinimumRelevant) 27 | 28 | XCTAssert(!title.trim.isEmpty) 29 | XCTAssert(!description.trim.isEmpty) 30 | } catch let err as NSError { 31 | print("\(err)") 32 | } 33 | } 34 | 35 | // MARK: - Amazon 36 | 37 | func testAmazonLinksWithGoogleBotUserAgent() throws { 38 | // Amazon links are huge and serve up very different html based on the user agent string 39 | // Some user agents don't contain og tags and will fail to locate title and images 40 | let amazonUrl = "https://www.amazon.com/Beginning-HTML5-CSS3-Dummies-Tittel/dp/1118657209/" 41 | let expectation = self.expectation(description: "Loading web page") 42 | var result: Response? 43 | 44 | let updatedSlp = SwiftLinkPreview(userAgent: SwiftLinkPreview.googleBotUserAgent) 45 | 46 | updatedSlp.preview(amazonUrl) { 47 | result = $0 48 | expectation.fulfill() 49 | } onError: { error in 50 | print(error) 51 | XCTAssertNil(error) 52 | } 53 | 54 | waitForExpectations(timeout: 15, handler: nil) 55 | 56 | let unwrappedResult = try XCTUnwrap(result) 57 | let title = try XCTUnwrap(unwrappedResult.title) 58 | 59 | XCTAssert(title.trim.isEmpty) 60 | XCTAssertNotNil(unwrappedResult.image) 61 | } 62 | 63 | func testAmazonLinksWithOriginalSlpUserAgent() throws { 64 | // Amazon links are huge and serve up very different html based on the user agent string 65 | // Some user agents don't contain og tags and will fail to locate title and images 66 | let amazonUrl = "https://www.amazon.com/Beginning-HTML5-CSS3-Dummies-Tittel/dp/1118657209/" 67 | let expectation = self.expectation(description: "Loading web page") 68 | var result: Response? 69 | 70 | slp.preview(amazonUrl) { 71 | result = $0 72 | expectation.fulfill() 73 | } onError: { error in 74 | print(error) 75 | XCTAssertNil(error) 76 | } 77 | 78 | waitForExpectations(timeout: 15, handler: nil) 79 | 80 | let unwrappedResult = try XCTUnwrap(result) 81 | let title = try XCTUnwrap(unwrappedResult.title) 82 | 83 | XCTAssert(title.trim.isEmpty) 84 | XCTAssertNotNil(unwrappedResult.image) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /SwiftLinkPreview.xcodeproj/xcshareddata/xcschemes/SwiftLinkPreviewWatchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 53 | 59 | 60 | 61 | 62 | 68 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Sources/Extensions/StringExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtension.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 09/06/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | #if os(iOS) || os(watchOS) || os(tvOS) 11 | 12 | import UIKit 13 | 14 | #elseif os(OSX) 15 | 16 | import Cocoa 17 | 18 | #endif 19 | 20 | extension String { 21 | // Trim 22 | var trim: String { 23 | return trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 24 | } 25 | 26 | // Remove extra white spaces 27 | var extendedTrim: String { 28 | let components = self.components(separatedBy: CharacterSet.whitespacesAndNewlines) 29 | return components.filter { !$0.isEmpty }.joined(separator: " ").trim 30 | } 31 | 32 | // Decode HTML entities 33 | var decoded: String { 34 | guard let encodedData = data(using: String.Encoding.utf8) else { return self } 35 | 36 | let attributedOptions: [NSAttributedString.DocumentReadingOptionKey: Any] = 37 | [ 38 | .documentType: NSAttributedString.DocumentType.html, 39 | .characterEncoding: NSNumber(value: String.Encoding.utf8.rawValue), 40 | ] 41 | 42 | do { 43 | let attributedString = try NSAttributedString( 44 | data: encodedData, 45 | options: attributedOptions, 46 | documentAttributes: nil 47 | ) 48 | 49 | return attributedString.string 50 | } catch _ { 51 | return self 52 | } 53 | } 54 | 55 | // Strip tags 56 | var tagsStripped: String { 57 | return deleteTagByPattern(Regex.rawTagPattern) 58 | } 59 | 60 | // Delete tab by pattern 61 | func deleteTagByPattern(_ pattern: String) -> String { 62 | return replacingOccurrences(of: pattern, with: "", options: .regularExpression, range: nil) 63 | } 64 | 65 | // Replace 66 | func replace(_ search: String, with: String) -> String { 67 | let replaced: String = replacingOccurrences(of: search, with: with) 68 | 69 | return replaced.isEmpty ? self : replaced 70 | } 71 | 72 | // Substring 73 | func substring(_ start: Int, end: Int) -> String { 74 | return substring(NSRange(location: start, length: end - start)) 75 | } 76 | 77 | func substring(_ range: NSRange) -> String { 78 | var end = range.location + range.length 79 | end = end > count ? count - 1 : end 80 | 81 | return substring(range.location, end: end) 82 | } 83 | 84 | // Check if url is an image 85 | func isImage() -> Bool { 86 | let possible = ["gif", "jpg", "jpeg", "png", "bmp"] 87 | if let url = URL(string: self), 88 | possible.contains(url.pathExtension) { 89 | return true 90 | } 91 | 92 | return false 93 | } 94 | 95 | func isOpenGraphImage() -> Bool { 96 | return Regex.test(self, regex: Regex.openGraphImagePattern) 97 | } 98 | 99 | func isVideo() -> Bool { 100 | let possible = ["mp4", "mov", "mpeg", "avi", "m3u8"] 101 | if let url = URL(string: self), 102 | possible.contains(url.pathExtension) { 103 | return true 104 | } 105 | 106 | return false 107 | } 108 | 109 | // Split into substring of equal length 110 | func split(by length: Int) -> [String] { 111 | var startIndex = self.startIndex 112 | var results = [Substring]() 113 | 114 | while startIndex < endIndex { 115 | let endIndex = index(startIndex, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex 116 | results.append(self[startIndex ..< endIndex]) 117 | startIndex = endIndex 118 | } 119 | 120 | return results.map { String($0) } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample.xcodeproj/xcshareddata/xcschemes/SwiftLinkPreviewExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 76 | 78 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /SwiftLinkPreview.xcodeproj/xcshareddata/xcschemes/SwiftLinkPreviewTvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /SwiftLinkPreview.xcodeproj/xcshareddata/xcschemes/SwiftLinkPreviewMacOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/ImageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageTests.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 05/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | @testable import SwiftLinkPreview 10 | import XCTest 11 | 12 | // This final class tests body images 13 | final class ImageTests: XCTestCase { 14 | // MARK: - Vars 15 | 16 | var singleImageTemplate = "" 17 | var galleryImageTemplate = "" 18 | let slp = SwiftLinkPreview() 19 | 20 | // MARK: - SetUps 21 | 22 | // Those setup functions get that template, and fulfil determinated areas with rand texts, images and tags 23 | override func setUp() { 24 | super.setUp() 25 | 26 | singleImageTemplate = File.toString(Constants.bodyImageSingle) 27 | galleryImageTemplate = File.toString(Constants.bodyImageGallery) 28 | } 29 | 30 | // MARK: - Single 31 | 32 | func setUpSingle() throws { 33 | let data = [Constants.image: String.randomImage()] 34 | 35 | var singleImageTemplate = self.singleImageTemplate 36 | singleImageTemplate = singleImageTemplate.replace(Constants.headRandom, with: String.randomTag()) 37 | singleImageTemplate = singleImageTemplate.replace(Constants.bodyRandomPre, with: String.randomTag()) 38 | singleImageTemplate = singleImageTemplate.replace(Constants.bodyRandomPos, with: String.randomTag()) 39 | 40 | singleImageTemplate = singleImageTemplate.replace(Constants.image, with: try XCTUnwrap(data[Constants.image])) 41 | 42 | singleImageTemplate = singleImageTemplate.replace(Constants.bodyRandom, with: String.randomTag()).extendedTrim 43 | 44 | let result = slp.crawlImages( 45 | singleImageTemplate, 46 | result: 47 | Response() 48 | ) 49 | 50 | XCTAssertEqual(result.image, data[Constants.image]) 51 | } 52 | 53 | func testSingle() throws { 54 | for _ in 0 ..< 100 { 55 | try setUpSingle() 56 | } 57 | } 58 | 59 | // MARK: - Gallery 60 | 61 | func setUpGallery() throws { 62 | let data = [ 63 | Constants.image1: String.randomImage(), 64 | Constants.image2: String.randomImage(), 65 | Constants.image3: String.randomImage(), 66 | ] 67 | 68 | var galleryImageTemplate = self.galleryImageTemplate 69 | galleryImageTemplate = galleryImageTemplate.replace(Constants.headRandom, with: String.randomTag()) 70 | galleryImageTemplate = galleryImageTemplate.replace(Constants.bodyRandomPre, with: String.randomTag()) 71 | galleryImageTemplate = galleryImageTemplate.replace(Constants.bodyRandomPos, with: String.randomTag()) 72 | 73 | galleryImageTemplate = galleryImageTemplate.replace( 74 | Constants.image1, 75 | with: try XCTUnwrap(data[Constants.image1]) 76 | ) 77 | galleryImageTemplate = galleryImageTemplate.replace( 78 | Constants.image2, 79 | with: try XCTUnwrap(data[Constants.image2]) 80 | ) 81 | galleryImageTemplate = galleryImageTemplate.replace( 82 | Constants.image3, 83 | with: try XCTUnwrap(data[Constants.image3]) 84 | ) 85 | 86 | galleryImageTemplate = galleryImageTemplate.replace(Constants.bodyRandom, with: String.randomTag()).extendedTrim 87 | 88 | let result = slp.crawlImages(galleryImageTemplate, result: Response()) 89 | 90 | XCTAssertEqual(result.images?[0], data[Constants.image1]) 91 | XCTAssertEqual(result.images?[1], data[Constants.image2]) 92 | XCTAssertEqual(result.images?[2], data[Constants.image3]) 93 | } 94 | 95 | func testGallery() throws { 96 | for _ in 0 ..< 100 { 97 | try setUpGallery() 98 | } 99 | } 100 | 101 | func testImgur() throws { 102 | do { 103 | let source = try String(contentsOf: try XCTUnwrap(URL(string: "https://imgur.com/GoAkW6w"))).extendedTrim 104 | 105 | let result = slp.crawlMetaTags(source, result: Response()) 106 | 107 | print(result) 108 | } catch let err as NSError { 109 | print("\(err)") 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Classes/Regex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Regex.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 09/06/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | // MARK: - Regular expressions 11 | 12 | enum Regex { 13 | public static var kLimit: Int = 1_000_000 14 | static let imagePattern = "(.+?)\\.(gif|jpg|jpeg|png|bmp)$" 15 | static let openGraphImagePattern = "(.+?)\\.(gif||jpg|jpeg|png|bmp)$" 16 | static let videoTagPattern = "]+src=\"([^\"]+)" 17 | static let secondaryVideoTagPattern = "og:video\"(.+?)content=\"([^\"](.+?))\"(.+?)[/]?>" 18 | static let imageTagPattern = "" 19 | static let secondaryImageTagPattern = "og:image\"(.+?)content=\"([^\"](.+?))\"(.+?)[/]?>" 20 | static let titlePattern = "(.*?)" 21 | static let metaTagPattern = "" 22 | static let baseTagPattern = "" 23 | static let metaTagContentPattern = "content=(\"(.*?)\")|('(.*?)')" 24 | static let cannonicalUrlPattern = "([^\\+&#@%\\?=~_\\|!:,;]+)" 25 | static let rawTagPattern = "<[^>]+>" 26 | static let inlineStylePattern = "(.*?)" 27 | static let inlineScriptPattern = "(.*?)" 28 | static let linkPattern = "" 29 | static let scriptPattern = "" 30 | static let commentPattern = "" 31 | static let hrefPattern = ".*href=\"(.*?)\".*" 32 | static let pricePattern = "itemprop=\"price\" content=\"([^\"]*)\"" 33 | 34 | // Test regular expression 35 | static func test(_ string: String, regex: String) -> Bool { 36 | Regex.pregMatchFirst(string, regex: regex) != nil 37 | } 38 | 39 | // Match first occurrency 40 | static func pregMatchFirst(_ string: String, regex: String, index: Int = 0) -> String? { 41 | do { 42 | let rx = try NSRegularExpression(pattern: regex, options: [.caseInsensitive]) 43 | 44 | if let match = rx.firstMatch(in: string, options: [], range: NSRange(string.startIndex..., in: string)) { 45 | let result: [String] = Regex.stringMatches([match], text: string, index: index) 46 | return result.isEmpty ? nil : result[0] 47 | } else { 48 | return nil 49 | } 50 | } catch { 51 | return nil 52 | } 53 | } 54 | 55 | // Match all occurrencies 56 | static func pregMatchAll(_ string: String, regex: String, index: Int = 0) -> [String] { 57 | do { 58 | let rx = try NSRegularExpression(pattern: regex, options: [.caseInsensitive]) 59 | 60 | var matches: [NSTextCheckingResult] = [] 61 | 62 | let limit = Regex.kLimit 63 | 64 | if string.count > limit { 65 | for item in string.split(by: limit) { 66 | matches.append(contentsOf: rx.matches( 67 | in: string, 68 | options: [], 69 | range: NSRange(item.startIndex..., in: item) 70 | )) 71 | } 72 | } else { 73 | matches.append(contentsOf: rx.matches( 74 | in: string, 75 | options: [], 76 | range: NSRange(string.startIndex..., in: string) 77 | )) 78 | } 79 | 80 | return !matches.isEmpty ? Regex.stringMatches(matches, text: string, index: index) : [] 81 | } catch { 82 | return [] 83 | } 84 | } 85 | 86 | // Extract matches from string 87 | static func stringMatches(_ results: [NSTextCheckingResult], text: String, index: Int = 0) -> [String] { 88 | return results.map { 89 | let range = $0.range(at: index) 90 | return if text.count > range.location + range.length { 91 | (text as NSString).substring(with: range) 92 | } else { 93 | "" 94 | } 95 | } 96 | } 97 | 98 | // Return tag pattern 99 | static func tagPattern(_ tag: String) -> String { 100 | return "<" + tag + "(.*?)>(.*?)" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Constants/URLs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLs.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 16/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum URLs { 12 | // Please add the text in the format: 13 | // [text, expectation, canonical] 14 | // ["xxx https://your.url.com/etc#something?else=true xxx", "https://your.url.com/etc#something?else=true", 15 | // "your.url.com"] 16 | static let bunch = [ 17 | [ 18 | "aaa www.google.com aaa", 19 | "http://www.google.com", 20 | "www.google.com", 21 | ], 22 | [ 23 | "bbb https://www.google.com.br/?gfe_rd=cr&ei=iVKKV7nXLcSm8wfE5InADg&gws_rd=ssl bbb", 24 | "https://www.google.com.br/?gfe_rd=cr&ei=iVKKV7nXLcSm8wfE5InADg&gws_rd=ssl", 25 | "www.google.com.br", 26 | ], 27 | [ 28 | "ccc123 http://google.com qwqwe", 29 | "http://google.com", 30 | "google.com", 31 | ], 32 | [ 33 | "ddd http://ios.leocardz.com/swift-link-preview/ ddd", 34 | "http://ios.leocardz.com/swift-link-preview/", 35 | "ios.leocardz.com", 36 | ], 37 | [ 38 | "ddd ios.leocardz.com/swift-link-preview/ ddd", 39 | "http://ios.leocardz.com/swift-link-preview/", 40 | "ios.leocardz.com", 41 | ], 42 | [ 43 | "ddd ios.leocardz.com ddd", 44 | "http://ios.leocardz.com", 45 | "ios.leocardz.com", 46 | ], 47 | [ 48 | "eee http://www.nasa.gov/ eee", 49 | "http://www.nasa.gov/", 50 | "www.nasa.gov", 51 | ], 52 | [ 53 | "fff theverge.com/2016/6/21/11996280/tesla-offer-solar-city-buy fff", 54 | "http://theverge.com/2016/6/21/11996280/tesla-offer-solar-city-buy", 55 | "theverge.com", 56 | ], 57 | [ 58 | "fff http://theverge.com/2016/6/21/11996280/tesla-offer-solar-city-buy fff", 59 | "http://theverge.com/2016/6/21/11996280/tesla-offer-solar-city-buy", 60 | "theverge.com", 61 | ], 62 | [ 63 | "ggg http://bit.ly/14SD1eR ggg", 64 | "http://bit.ly/14SD1eR", 65 | "bit.ly", 66 | ], 67 | [ 68 | "hhh https://twitter.com hhh", 69 | "https://twitter.com", 70 | "twitter.com", 71 | ], 72 | [ 73 | "hhh twitter.com hhh", 74 | "http://twitter.com", 75 | "twitter.com", 76 | ], 77 | [ 78 | "iii https://www.nationalgallery.org.uk#2123123?sadasd&asd iii", 79 | "https://www.nationalgallery.org.uk#2123123?sadasd&asd", 80 | "www.nationalgallery.org.uk", 81 | ], 82 | [ 83 | "jjj http://globo.com jjj", 84 | "http://globo.com", 85 | "globo.com", 86 | ], 87 | [ 88 | "kkk http://uol.com.br kkk", 89 | "http://uol.com.br", 90 | "uol.com.br", 91 | ], 92 | [ 93 | "lll http://vnexpress.net/ lll", 94 | "http://vnexpress.net/", 95 | "vnexpress.net", 96 | ], 97 | [ 98 | "mmm http://www3.nhk.or.jp/ mmm", 99 | "http://www3.nhk.or.jp/", 100 | "www3.nhk.or.jp", 101 | ], 102 | [ 103 | "nnn http://habrahabr.ru nnn", 104 | "http://habrahabr.ru", 105 | "habrahabr.ru", 106 | ], 107 | [ 108 | "ooo http://www.youtube.com/watch?v=cv2mjAgFTaI ooo", 109 | "http://www.youtube.com/watch?v=cv2mjAgFTaI", 110 | "www.youtube.com", 111 | ], 112 | [ 113 | "ppp http://vimeo.com/67992157 ppp", 114 | "http://vimeo.com/67992157", 115 | "vimeo.com", 116 | ], 117 | [ 118 | "qqq https://lh6.googleusercontent.com/-aDALitrkRFw/UfQEmWPMQnI/AAAAAAAFOlQ/mDh1l4ej15k/w337-h697-no/db1969caa4ecb88ef727dbad05d5b5b3.jpg qqq", 119 | "https://lh6.googleusercontent.com/-aDALitrkRFw/UfQEmWPMQnI/AAAAAAAFOlQ/mDh1l4ej15k/w337-h697-no/db1969caa4ecb88ef727dbad05d5b5b3.jpg", 120 | "lh6.googleusercontent.com", 121 | ], 122 | [ 123 | "rrr http://goo.gl/jKCPgp rrr", 124 | "http://goo.gl/jKCPgp", 125 | "goo.gl", 126 | ], 127 | ] 128 | } 129 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/Utils/StringTestExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringTestExtension.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 05/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import GameplayKit 11 | 12 | extension String { 13 | static let loremIpsum = 14 | [ 15 | "Et harum quidem rerum facilis est et expedita distinctio.", 16 | "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", 17 | "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 18 | "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 19 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 20 | "Temporibus autem quibusdam et aut officiis debitis aut rerum &necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae.", 21 | "Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.", 22 | "At vero eos et accusamus et iusto odio dignissimos’ ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga.", 23 | "Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.", 24 | "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.", 25 | "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.", 26 | "Neque porro quisquam est’, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", 27 | "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur?", 28 | "Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?", 29 | ] 30 | 31 | static let protocolType = ["http://", "https://"] 32 | static let tagType = ["span", "p", "div"] 33 | static let imageType = ["gif", "jpg", "jpeg", "png", "bmp"] 34 | 35 | // Random String 36 | static func randomString(_ length: Int) -> String { 37 | let charactersString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 38 | let charactersArray = charactersString.map { String($0) } 39 | 40 | var string = "" 41 | for _ in 0 ..< length { 42 | string += charactersArray[Int.random(upper: charactersArray.count)] 43 | } 44 | 45 | return string 46 | } 47 | 48 | // Random Text 49 | static func randomText() -> String { 50 | return loremIpsum[Int.random(upper: loremIpsum.count)] 51 | } 52 | 53 | // Random Tag 54 | static func randomTag() -> String { 55 | let tag = tagType[Int.random(upper: tagType.count)] 56 | return "<\(tag)>\(randomText())" 57 | } 58 | 59 | // Random Tag 60 | static func randomImageTag() -> String { 61 | let options = [ 62 | "", 63 | "", 64 | "", 65 | ] 66 | 67 | return options[Int.random(upper: options.count)] 68 | } 69 | 70 | // Random URL 71 | static func randomUrl() -> String { 72 | var rand = Int.random(upper: 30) + 5 73 | let base = String.randomString(rand) 74 | 75 | rand = Int.random(upper: 3) + 2 76 | rand = rand.hashValue > 3 ? 3 : rand 77 | let end = String.randomString(rand) 78 | 79 | let prtcl = protocolType[Int.random(upper: protocolType.count)] 80 | return "\(prtcl)\(base).\(end.lowercased())" 81 | } 82 | 83 | // Random Image 84 | static func randomImage() -> String { 85 | return "\(randomUrl())/\(String.randomString(Int.random(upper: 15) + 5)).\(imageType[Int.random(upper: imageType.count)])" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /SwiftLinkPreview.xcodeproj/xcshareddata/xcschemes/SwiftLinkPreview.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 77 | 83 | 84 | 85 | 86 | 92 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/BodyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BodyTests.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 05/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | @testable import SwiftLinkPreview 10 | import XCTest 11 | 12 | // This class tests body texts 13 | final class BodyTests: XCTestCase { 14 | // MARK: - Vars 15 | 16 | var spanTemplate = "" 17 | var pTemplate = "" 18 | var divTemplate = "" 19 | let slp = SwiftLinkPreview() 20 | 21 | // MARK: - SetUps 22 | 23 | // Those setup functions get that template, and fulfil determinated areas with rand texts, images and tags 24 | override func setUp() { 25 | super.setUp() 26 | 27 | spanTemplate = File.toString(Constants.bodyTextSpan) 28 | divTemplate = File.toString(Constants.bodyTextDiv) 29 | pTemplate = File.toString(Constants.bodyTextP) 30 | } 31 | 32 | // MARK: - Span 33 | 34 | func setUpSpan() throws { 35 | let metaData = 36 | [ 37 | Constants.random1: String.randomText(), 38 | Constants.random2: String.randomText(), 39 | ] 40 | 41 | var template = spanTemplate 42 | template = template.replace(Constants.headRandom, with: String.randomText()) 43 | 44 | let random1 = try XCTUnwrap(metaData[Constants.random1]) 45 | let random2 = try XCTUnwrap(metaData[Constants.random2]) 46 | template = template.replace(Constants.random1, with: random1) 47 | template = template.replace(Constants.random2, with: random2) 48 | 49 | template = template.replace(Constants.tag1, with: String.randomText()) 50 | template = template.replace(Constants.tag2, with: String.randomText()) 51 | 52 | template = template.replace(Constants.bodyRandomPre, with: String.randomText()) 53 | template = template.replace(Constants.bodyRandomMiddle, with: String.randomText()) 54 | template = template.replace(Constants.bodyRandomPos, with: String.randomText()).extendedTrim 55 | 56 | let response = slp.crawlDescription(template, result: Response()) 57 | 58 | let comparable = response.result.description 59 | 60 | XCTAssert(comparable == random1.decoded || comparable == random2.decoded) 61 | } 62 | 63 | func testSpan() throws { 64 | for _ in 0 ..< 100 { 65 | try setUpSpan() 66 | } 67 | } 68 | 69 | // MARK: - Div 70 | 71 | func setUpDiv() throws { 72 | let metaData = 73 | [ 74 | Constants.random1: String.randomText(), 75 | Constants.random2: String.randomText(), 76 | ] 77 | 78 | var template = divTemplate 79 | template = template.replace(Constants.headRandom, with: String.randomText()) 80 | 81 | let random1 = try XCTUnwrap(metaData[Constants.random1]) 82 | let random2 = try XCTUnwrap(metaData[Constants.random2]) 83 | template = template.replace(Constants.random1, with: random1) 84 | template = template.replace(Constants.random2, with: random2) 85 | 86 | template = template.replace(Constants.tag1, with: String.randomText()) 87 | template = template.replace(Constants.tag2, with: String.randomText()) 88 | 89 | template = template.replace(Constants.bodyRandomPre, with: String.randomText()) 90 | template = template.replace(Constants.bodyRandomMiddle, with: String.randomText()) 91 | template = template.replace(Constants.bodyRandomPos, with: String.randomText()).extendedTrim 92 | 93 | let response = slp.crawlDescription(template, result: Response()) 94 | 95 | let comparable = response.result.description 96 | 97 | XCTAssert( 98 | comparable == random1.decoded || comparable == random2.decoded 99 | ) 100 | } 101 | 102 | func testDiv() throws { 103 | for _ in 0 ..< 100 { 104 | try setUpDiv() 105 | } 106 | } 107 | 108 | // MARK: - P 109 | 110 | func setUpP() throws { 111 | let metaData = 112 | [ 113 | Constants.random1: String.randomText(), 114 | Constants.random2: String.randomText(), 115 | ] 116 | 117 | var template = pTemplate 118 | template = template.replace(Constants.headRandom, with: String.randomText()) 119 | 120 | let random1 = try XCTUnwrap(metaData[Constants.random1]) 121 | let random2 = try XCTUnwrap(metaData[Constants.random2]) 122 | template = template.replace(Constants.random1, with: random1) 123 | template = template.replace(Constants.random2, with: random2) 124 | 125 | template = template.replace(Constants.tag1, with: String.randomText()) 126 | template = template.replace(Constants.tag2, with: String.randomText()) 127 | 128 | template = template.replace(Constants.bodyRandomPre, with: String.randomText()) 129 | template = template.replace(Constants.bodyRandomMiddle, with: String.randomText()) 130 | template = template.replace(Constants.bodyRandomPos, with: String.randomText()).extendedTrim 131 | 132 | let response = slp.crawlDescription(template, result: Response()) 133 | 134 | let comparable = response.result.description 135 | 136 | XCTAssert( 137 | comparable == random1.decoded || comparable == random2.decoded 138 | ) 139 | } 140 | 141 | func testP() throws { 142 | for _ in 0 ..< 100 { 143 | try setUpP() 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /SwiftLinkPreviewTests/MetaTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetaTests.swift 3 | // SwiftLinkPreview 4 | // 5 | // Created by Leonardo Cardoso on 05/07/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | @testable import SwiftLinkPreview 10 | import XCTest 11 | 12 | // This class tests head meta info 13 | final class MetaTests: XCTestCase { 14 | // MARK: - Vars 15 | 16 | var twitterTemplate = "" 17 | var facebookTemplate = "" 18 | var itempropTemplate = "" 19 | var metaTemplate = "" 20 | let slp = SwiftLinkPreview() 21 | 22 | // MARK: - SetUps 23 | 24 | override func setUpWithError() throws { 25 | try super.setUpWithError() 26 | 27 | twitterTemplate = try File.toString(Constants.headMetaTwitter) 28 | facebookTemplate = try File.toString(Constants.headMetaFacebook) 29 | itempropTemplate = try File.toString(Constants.headMetaItemprop) 30 | metaTemplate = try File.toString(Constants.headMetaMeta) 31 | } 32 | 33 | // MARK: - Twitter 34 | 35 | func setUpTwitterAndRun() throws { 36 | let twitterData = 37 | [ 38 | Constants.twitterTitle: String.randomText(), 39 | Constants.twitterSite: String.randomUrl(), 40 | Constants.twitterDescription: String.randomText(), 41 | Constants.twitterImageSrc: String.randomImage(), 42 | ] 43 | 44 | let twitterTitle = try XCTUnwrap(twitterData[Constants.twitterTitle]) 45 | let twitterSite = try XCTUnwrap(twitterData[Constants.twitterSite]) 46 | let twitterDescription = try XCTUnwrap(twitterData[Constants.twitterDescription]) 47 | let twitterImageSrc = try XCTUnwrap(twitterData[Constants.twitterImageSrc]) 48 | 49 | var twitterTemplate = self.twitterTemplate 50 | twitterTemplate = twitterTemplate.replace(Constants.headRandomPre, with: String.randomTag()) 51 | twitterTemplate = twitterTemplate.replace(Constants.headRandomPos, with: String.randomTag()) 52 | 53 | twitterTemplate = twitterTemplate.replace(Constants.twitterTitle, with: twitterTitle) 54 | twitterTemplate = twitterTemplate.replace(Constants.twitterSite, with: twitterSite) 55 | twitterTemplate = twitterTemplate.replace(Constants.twitterDescription, with: twitterDescription) 56 | twitterTemplate = twitterTemplate.replace(Constants.twitterImageSrc, with: twitterImageSrc) 57 | 58 | twitterTemplate = twitterTemplate.replace(Constants.bodyRandom, with: String.randomTag()).extendedTrim 59 | 60 | let result = slp.crawlMetaTags(twitterTemplate, result: Response()) 61 | 62 | XCTAssertEqual(result.title, twitterTitle.decoded) 63 | XCTAssertEqual(result.description, twitterDescription.decoded) 64 | XCTAssertEqual(result.image, twitterImageSrc) 65 | } 66 | 67 | func testTwitter() throws { 68 | for _ in 0 ..< 100 { 69 | try setUpTwitterAndRun() 70 | } 71 | } 72 | 73 | // MARK: - Facebook 74 | 75 | func setUpFacebookAndRun() throws { 76 | let facebookData = 77 | [ 78 | Constants.facebookTitle: String.randomText(), 79 | Constants.facebookSite: String.randomUrl(), 80 | Constants.facebookDescription: String.randomText(), 81 | Constants.facebookImage: String.randomImage(), 82 | ] 83 | 84 | let facebookTitle = try XCTUnwrap(facebookData[Constants.facebookTitle]) 85 | let facebookSite = try XCTUnwrap(facebookData[Constants.facebookSite]) 86 | let facebookDescription = try XCTUnwrap(facebookData[Constants.facebookDescription]) 87 | let facebookImage = try XCTUnwrap(facebookData[Constants.facebookImage]) 88 | 89 | var facebookTemplate = self.facebookTemplate 90 | facebookTemplate = facebookTemplate.replace(Constants.headRandomPre, with: String.randomTag()) 91 | facebookTemplate = facebookTemplate.replace(Constants.headRandomPos, with: String.randomTag()) 92 | 93 | facebookTemplate = facebookTemplate.replace(Constants.facebookTitle, with: facebookTitle) 94 | facebookTemplate = facebookTemplate.replace(Constants.facebookSite, with: facebookSite) 95 | facebookTemplate = facebookTemplate.replace(Constants.facebookDescription, with: facebookDescription) 96 | facebookTemplate = facebookTemplate.replace(Constants.facebookImage, with: facebookImage) 97 | 98 | facebookTemplate = facebookTemplate.replace(Constants.bodyRandom, with: String.randomTag()).extendedTrim 99 | 100 | let result = slp.crawlMetaTags(facebookTemplate, result: Response()) 101 | 102 | XCTAssertEqual(result.title, facebookTitle.decoded) 103 | XCTAssertEqual(result.description, facebookDescription.decoded) 104 | XCTAssertEqual(result.image, facebookImage) 105 | } 106 | 107 | func testFacebook() throws { 108 | for _ in 0 ..< 100 { 109 | try setUpFacebookAndRun() 110 | } 111 | } 112 | 113 | // MARK: - Itemprop 114 | 115 | func setUpItempropAndRun() throws { 116 | let itempropData = 117 | [ 118 | Constants.title: String.randomText(), 119 | Constants.site: String.randomUrl(), 120 | Constants.description: String.randomText(), 121 | Constants.image: String.randomImage(), 122 | ] 123 | 124 | let title = try XCTUnwrap(itempropData[Constants.title]) 125 | let site = try XCTUnwrap(itempropData[Constants.site]) 126 | let description = try XCTUnwrap(itempropData[Constants.description]) 127 | let image = try XCTUnwrap(itempropData[Constants.image]) 128 | 129 | var itempropTemplate = self.itempropTemplate 130 | itempropTemplate = itempropTemplate.replace(Constants.headRandomPre, with: String.randomTag()) 131 | itempropTemplate = itempropTemplate.replace(Constants.headRandomPos, with: String.randomTag()) 132 | 133 | itempropTemplate = itempropTemplate.replace(Constants.title, with: title) 134 | itempropTemplate = itempropTemplate.replace(Constants.site, with: site) 135 | itempropTemplate = itempropTemplate.replace(Constants.description, with: description) 136 | itempropTemplate = itempropTemplate.replace(Constants.image, with: image) 137 | 138 | itempropTemplate = itempropTemplate.replace(Constants.bodyRandom, with: String.randomTag()).extendedTrim 139 | 140 | let result = slp.crawlMetaTags(itempropTemplate, result: Response()) 141 | 142 | XCTAssertEqual(result.title, title.decoded) 143 | XCTAssertEqual(result.description, description.decoded) 144 | XCTAssertEqual(result.image, image) 145 | } 146 | 147 | func testItemprop() throws { 148 | for _ in 0 ..< 100 { 149 | try setUpItempropAndRun() 150 | } 151 | } 152 | 153 | // MARK: - Meta 154 | 155 | func setUpMetaAndRun() throws { 156 | let metaData = 157 | [ 158 | Constants.title: String.randomText(), 159 | Constants.site: String.randomUrl(), 160 | Constants.description: String.randomText(), 161 | Constants.image: String.randomImage(), 162 | ] 163 | 164 | let title = try XCTUnwrap(metaData[Constants.title]) 165 | let site = try XCTUnwrap(metaData[Constants.site]) 166 | let description = try XCTUnwrap(metaData[Constants.description]) 167 | let image = try XCTUnwrap(metaData[Constants.image]) 168 | 169 | var metaTemplate = self.metaTemplate 170 | metaTemplate = metaTemplate.replace(Constants.headRandomPre, with: String.randomTag()) 171 | metaTemplate = metaTemplate.replace(Constants.headRandomPos, with: String.randomTag()) 172 | 173 | metaTemplate = metaTemplate.replace(Constants.title, with: title) 174 | metaTemplate = metaTemplate.replace(Constants.site, with: site) 175 | metaTemplate = metaTemplate.replace(Constants.description, with: description) 176 | metaTemplate = metaTemplate.replace(Constants.image, with: image) 177 | 178 | metaTemplate = metaTemplate.replace(Constants.bodyRandom, with: String.randomTag()).extendedTrim 179 | 180 | let result = slp.crawlMetaTags(metaTemplate, result: Response()) 181 | 182 | XCTAssertEqual(result.title, title.decoded) 183 | XCTAssertEqual(result.description, description.decoded) 184 | XCTAssertEqual(result.image, image) 185 | } 186 | 187 | func testMeta() throws { 188 | for _ in 0 ..< 100 { 189 | try setUpMetaAndRun() 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Swift Link Preview](Images/badge.png) 2 | 3 | **Link Previewer** for **iOS**, **macOS**, **watchOS** and **tvOS** 4 | 5 | > It makes a preview from an URL, grabbing all the information such as title, relevant texts and images. 6 | 7 | > Update 2025: I've decided to archive this repo due to the fact that any AI integration performs a job better than SLP. It was a nice concept at the beginning and it served its purpose. 🫡 8 | 9 | [![Platform](https://img.shields.io/badge/platform-iOS%20|%20macOS%20|%20watchOS%20|%20tvOS-orange.svg)](https://github.com/LeonardoCardoso/SwiftLinkPreview#requirements-and-details) 10 | [![CocoaPods](https://img.shields.io/badge/pod-v3.5.0-red.svg)](https://github.com/LeonardoCardoso/SwiftLinkPreview#cocoapods) 11 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg)](https://github.com/LeonardoCardoso/SwiftLinkPreview#carthage) 12 | [![Swift Package Manager](https://img.shields.io/badge/SPM-compatible-orange.svg)](https://github.com/LeonardoCardoso/SwiftLinkPreview#swift-package-manager) 13 | [![Build Status](https://travis-ci.org/LeonardoCardoso/SwiftLinkPreview.svg?branch=master)](https://travis-ci.org/LeonardoCardoso/SwiftLinkPreview) 14 | 15 | #### Index 16 | 17 | * [Visual Examples](#visual-examples) 18 | * [Requirements and Details](#requirements-and-details) 19 | * [Installation](#installation) 20 | * [CocoaPods](#cocoapods) 21 | * [Carthage](#carthage) 22 | * [Swift Package Manager](#swift-package-manager) 23 | * [Manually](#manually) 24 | * [Usage](#usage) 25 | * [Instatiating](#instatiating) 26 | * [Requesting preview](#requesting-preview) 27 | * [Cancelling a request](#cancelling-a-request) 28 | * [Flow](#flow) 29 | * [Important](#important) 30 | * [Tips](#tips) 31 | * [Information and Contact](#information-and-contact) 32 | * [Related Projects](#related-projects) 33 | * [License](#license) 34 | 35 |
36 | 37 | ## Visual Examples 38 | 39 | **UTF-8** | **Extended UTF-8** | **Gallery** 40 | :--:|:--:|:--:| 41 | ![UTF-8](Images/default.gif "UTF-8") | ![Extended UTF-8](Images/langs.gif "Extended UTF-8") |![Gallery](Images/gallery.gif "Gallery") 42 | **Video Websites** | **Images** 43 | ![Video Websites](Images/videos.gif "Video Websites") | ![Images](Images/images.gif "Images") | 44 | 45 | ## Requirements and Details 46 | 47 | * iOS 8.0+ / macOS 10.11+ / tvOS 9.0+ / watchOS 2.0+ 48 | * Xcode 8.0+ 49 | * Built with Swift 5 50 | 51 | ## Installation 52 | 53 | ### CocoaPods 54 | 55 | To use **SwiftLinkPreview** as a pod package just add the following in your **Podfile** file. 56 | 57 | ```ruby 58 | source 'https://github.com/CocoaPods/Specs.git' 59 | platform :ios, '9.0' 60 | 61 | target 'Your Target Name' do 62 | use_frameworks! 63 | // ... 64 | pod 'SwiftLinkPreview', '~> 3.5.0' 65 | // ... 66 | end 67 | ``` 68 | 69 | ### Carthage 70 | 71 | To use **SwiftLinkPreview** as a Carthage module package just add the following in your **Cartfile** file. 72 | 73 | ```ruby 74 | // ... 75 | github "LeonardoCardoso/SwiftLinkPreview" ~> 3.5.0 76 | // ... 77 | ``` 78 | 79 | ### Swift Package Manager 80 | 81 | To use **SwiftLinkPreview** as a Swift Package Manager package just add the following in your **Package.swift** file. 82 | 83 | ```swift 84 | import PackageDescription 85 | 86 | let package = Package( 87 | name: "Your Target Name", 88 | dependencies: [ 89 | // ... 90 | .Package(url: "https://github.com/LeonardoCardoso/SwiftLinkPreview.git", "3.5.0") 91 | // ... 92 | ] 93 | ) 94 | ``` 95 | 96 | ### Manually 97 | 98 | You just need to drop all contents in `Sources` folder, **but .plist files**, into Xcode project (make sure to enable "Copy items if needed" and "Create groups"). 99 | 100 | 101 | ## Usage 102 | 103 | #### Instatiating 104 | ```swift 105 | import SwiftLinkPreview 106 | 107 | // ... 108 | 109 | let slp = SwiftLinkPreview(session: URLSession.shared, 110 | workQueue: SwiftLinkPreview.defaultWorkQueue, 111 | responseQueue: DispatchQueue.main, 112 | cache: DisabledCache.instance) 113 | ``` 114 | 115 | #### Requesting preview 116 | ```swift 117 | let preview = slp.preview("Text containing URL", 118 | onSuccess: { result in print("\(result)") }, 119 | onError: { error in print("\(error)")}) 120 | // preview.cancel() to cancel it. 121 | ``` 122 | **result** is a struct ```Response```: 123 | 124 | ```swift 125 | Response { 126 | let baseURL: String? // base 127 | let url: URL? // URL 128 | let finalUrl: URL? // unshortened URL 129 | let canonicalUrl: String? // canonical URL 130 | let title: String? // title 131 | let description: String? // page description or relevant text 132 | let images: [String]? // array of URLs of the images 133 | let image: String? // main image 134 | let icon: String? // favicon 135 | let video: String? // video 136 | let price: String? // price 137 | } 138 | ``` 139 | 140 | #### Cancelling a request 141 | ```swift 142 | let cancelablePreview = slp.preview(..., 143 | onSuccess: ..., 144 | onError: ...) 145 | 146 | cancelablePreview.cancel() 147 | ``` 148 | 149 | #### Enabling and accessing cache 150 | 151 | SLP has a built-in memory cache, so create your object as the following: 152 | 153 | ```swift 154 | let slp = SwiftLinkPreview(cache: InMemoryCache()) 155 | ``` 156 | 157 | To get the cached response: 158 | 159 | ```swift 160 | if let cached = self.slp.cache.slp_getCachedResponse(url: String) { 161 | // Do whatever with the cached response 162 | } else { 163 | // Perform preview otherwise 164 | slp.preview(...) 165 | } 166 | ``` 167 | 168 | If you want to create your own cache, just implement this protocol and use it on the object initializer. 169 | 170 | ```swift 171 | public protocol Cache { 172 | 173 | func slp_getCachedResponse(url: String) -> SwiftLinkPreview.Response? 174 | 175 | func slp_setCachedResponse(url: String, response: SwiftLinkPreview.Response?) 176 | } 177 | ``` 178 | 179 | # FLOW 180 | 181 | ![flow](http://i.imgur.com/SMueQkA.png) 182 | 183 | ## Important 184 | 185 | You need to set ```Allow Arbitrary Loads``` to ```YES``` on your project's Info.plist file. 186 | 187 | ![app security](http://i.imgur.com/41hGjCC.png) 188 | 189 | If you don't want to use the option above and you are using ```SwiftLinkPreview``` for services of your knowledge, you can whitelist them on Info.plist file as well. You can read more about it [here](http://stackoverflow.com/questions/30739473/nsurlsession-nsurlconnection-http-load-failed-on-ios-9), [here](https://github.com/Alamofire/Alamofire#app-transport-security) and [here](https://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/). 190 | 191 | ![app security](http://i.imgur.com/INEp6q5.png) 192 | 193 | ## Tips 194 | 195 | Not all websites will have their info brought, you can treat the info that your implementation gets as you like. But here are two tips about posting a preview: 196 | 197 | * If some info is missing, you can offer the user to enter it. Take for example the description. 198 | * If more than one image is fetched, you can offer the user the feature of picking one image. 199 | 200 | ## Tests 201 | 202 | Feel free to fork this repo and add URLs on [SwiftLinkPreviewTests/URLs.swift](SwiftLinkPreviewTests/URLs.swift) to test URLs and help improving this lib. The more URLs the better the reliability. 203 | 204 | ## Information and Contact 205 | 206 | Developed by [@LeonardoCardoso](https://github.com/LeonardoCardoso). 207 | 208 | Contact me either by Twitter [@leocardz](https://twitter.com/leocardz) or emailing me to [contact@leocardz.com](mailto:contact@leocardz.com). 209 | 210 | ## Related Projects 211 | 212 | * [Link Preview (PHP + Angular + Bootstrap)](https://github.com/LeonardoCardoso/Link-Preview) 213 | * [Android Link Preview](https://github.com/LeonardoCardoso/Android-Link-Preview) 214 | 215 | 216 | ## License 217 | 218 | The MIT License (MIT) 219 | 220 | Copyright (c) 2016 Leonardo Cardoso 221 | 222 | Permission is hereby granted, free of charge, to any person obtaining a copy 223 | of this software and associated documentation files (the "Software"), to deal 224 | in the Software without restriction, including without limitation the rights 225 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 226 | copies of the Software, and to permit persons to whom the Software is 227 | furnished to do so, subject to the following conditions: 228 | 229 | The above copyright notice and this permission notice shall be included in all 230 | copies or substantial portions of the Software. 231 | 232 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 233 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 234 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 235 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 236 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 237 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 238 | SOFTWARE. 239 | 240 | ### Follow me for the latest updates 241 | 242 | -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample/Controllers/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftLinkPreviewExample 4 | // 5 | // Created by Leonardo Cardoso on 09/06/2016. 6 | // Copyright © 2016 leocardz.com. All rights reserved. 7 | // 8 | 9 | import ImageSlideshow 10 | import SwiftLinkPreview 11 | import SwiftyDrop 12 | import UIKit 13 | 14 | class ViewController: UIViewController { 15 | // MARK: - Properties 16 | 17 | @IBOutlet private var centerLoadingActivityIndicatorView: UIActivityIndicatorView? 18 | @IBOutlet private var textField: UITextField? 19 | @IBOutlet private var randomTextButton: UIButton? 20 | @IBOutlet private var submitButton: UIButton? 21 | @IBOutlet private var openWithButton: UIButton? 22 | @IBOutlet private var indicator: UIActivityIndicatorView? 23 | @IBOutlet private var previewArea: UIView? 24 | @IBOutlet private var previewAreaLabel: UILabel? 25 | @IBOutlet private var slideshow: ImageSlideshow? 26 | @IBOutlet private var previewTitle: UILabel? 27 | @IBOutlet private var previewCanonicalUrl: UILabel? 28 | @IBOutlet private var previewDescription: UILabel? 29 | @IBOutlet private var detailedView: UIView? 30 | @IBOutlet private var favicon: UIImageView? 31 | 32 | // MARK: - Vars 33 | 34 | private var randomTexts: [String] = [ 35 | "blinkist.com", 36 | "uber.com", 37 | "tw.yahoo.com", 38 | "https://www.linkedin.com/", 39 | "www.youtube.com", 40 | "www.google.com", 41 | "facebook.com", 42 | 43 | "https://github.com/LeonardoCardoso/SwiftLinkPreview", 44 | "https://www.jbhifi.com.au/products/playstation-4-biomutant", 45 | 46 | "https://leocardz.com/swift-link-preview-5a9860c7756f", 47 | "NASA! 🖖🏽 https://www.nasa.gov/", 48 | "https://www.theverge.com/2016/6/21/11996280/tesla-offer-solar-city-buy", 49 | "Shorten URL http://bit.ly/14SD1eR", 50 | "Tweet! https://twitter.com", 51 | 52 | "A Gallery https://www.nationalgallery.org.uk", 53 | "www.dji.com/matrice600-pro/info#specs", 54 | 55 | "A Brazilian website http://globo.com", 56 | "Another Brazilian website https://uol.com.br", 57 | "Some Vietnamese chars https://vnexpress.net/", 58 | "Japan!!! https://www3.nhk.or.jp/", 59 | "A Russian website >> https://habrahabr.ru", 60 | 61 | "Youtube?! It does! https://www.youtube.com/watch?v=cv2mjAgFTaI", 62 | "Also Vimeo https://vimeo.com/67992157", 63 | 64 | "Well, it's a gif! https://goo.gl/jKCPgp", 65 | ] 66 | 67 | private var result = Response() 68 | private let placeholderImages = [ImageSource(image: UIImage(named: "Placeholder")!)] 69 | 70 | private let slp = SwiftLinkPreview(cache: InMemoryCache()) 71 | 72 | // MARK: - Life cycle 73 | 74 | override func viewDidLoad() { 75 | super.viewDidLoad() 76 | // Do any additional setup after loading the view, typically from a nib. 77 | 78 | showHideAll(hide: true) 79 | setUpSlideshow() 80 | } 81 | 82 | override func didReceiveMemoryWarning() { 83 | super.didReceiveMemoryWarning() 84 | // Dispose of any resources that can be recreated. 85 | } 86 | 87 | private func getRandomText() -> String { 88 | return randomTexts[Int(arc4random_uniform(UInt32(randomTexts.count)))] 89 | } 90 | 91 | private func startCrawling() { 92 | centerLoadingActivityIndicatorView?.startAnimating() 93 | updateUI(enabled: false) 94 | showHideAll(hide: true) 95 | textField?.resignFirstResponder() 96 | indicator?.isHidden = false 97 | } 98 | 99 | private func endCrawling() { 100 | updateUI(enabled: true) 101 | } 102 | 103 | // Update UI 104 | private func showHideAll(hide: Bool) { 105 | slideshow?.isHidden = hide 106 | detailedView?.isHidden = hide 107 | openWithButton?.isHidden = hide 108 | previewAreaLabel?.isHidden = !hide 109 | } 110 | 111 | private func updateUI(enabled: Bool) { 112 | indicator?.isHidden = enabled 113 | textField?.isEnabled = enabled 114 | randomTextButton?.isEnabled = enabled 115 | submitButton?.isEnabled = enabled 116 | } 117 | 118 | private func setData() { 119 | if let value = result.images { 120 | if !value.isEmpty { 121 | var images: [InputSource] = [] 122 | for image in value { 123 | if let source = AlamofireSource(urlString: image) { 124 | images.append(source) 125 | } 126 | } 127 | 128 | setImage(images: images) 129 | } else { 130 | setImage(image: result.image) 131 | } 132 | } else { 133 | setImage(image: result.image) 134 | } 135 | 136 | if let value: String = result.title { 137 | previewTitle?.text = value.isEmpty ? "No title" : value 138 | } else { 139 | previewTitle?.text = "No title" 140 | } 141 | 142 | if let value: String = result.canonicalUrl { 143 | previewCanonicalUrl?.text = value 144 | } 145 | 146 | if let value: String = result.description { 147 | previewDescription?.text = value.isEmpty ? "No description" : value 148 | } else { 149 | previewTitle?.text = "No description" 150 | } 151 | 152 | if let value: String = result.icon, let url = URL(string: value) { 153 | favicon?.af.setImage(withURL: url) 154 | } 155 | 156 | showHideAll(hide: false) 157 | endCrawling() 158 | } 159 | 160 | private func setImage(image: String?) { 161 | if let image: String = image { 162 | if !image.isEmpty { 163 | if let source = AlamofireSource(urlString: image) { 164 | setImage(images: [source]) 165 | } else { 166 | slideshow?.setImageInputs(placeholderImages) 167 | } 168 | } else { 169 | slideshow?.setImageInputs(placeholderImages) 170 | } 171 | } else { 172 | slideshow?.setImageInputs(placeholderImages) 173 | } 174 | 175 | centerLoadingActivityIndicatorView?.stopAnimating() 176 | } 177 | 178 | private func setImage(images: [InputSource]?) { 179 | if let images = images { 180 | slideshow?.setImageInputs(images) 181 | } else { 182 | slideshow?.setImageInputs(placeholderImages) 183 | } 184 | 185 | centerLoadingActivityIndicatorView?.stopAnimating() 186 | } 187 | 188 | private func setUpSlideshow() { 189 | slideshow?.backgroundColor = UIColor.white 190 | slideshow?.slideshowInterval = 7.0 191 | slideshow?.pageIndicatorPosition = .init(horizontal: .center, vertical: .bottom) 192 | slideshow?.contentScaleMode = .scaleAspectFill 193 | } 194 | 195 | // MARK: - Actions 196 | 197 | @IBAction func randomTextAction(_ sender: AnyObject) { 198 | textField?.text = getRandomText() 199 | } 200 | 201 | @IBAction func submitAction(_ sender: AnyObject) { 202 | func printResult(_ result: Response) { 203 | print("url: ", result.url ?? "no url") 204 | print("finalUrl: ", result.finalUrl ?? "no finalUrl") 205 | print("canonicalUrl: ", result.canonicalUrl ?? "no canonicalUrl") 206 | print("title: ", result.title ?? "no title") 207 | print("images: ", result.images ?? "no images") 208 | print("image: ", result.image ?? "no image") 209 | print("video: ", result.video ?? "no video") 210 | print("icon: ", result.icon ?? "no icon") 211 | print("description: ", result.description ?? "no description") 212 | print("baseURL: ", result.baseURL ?? "no baseURL") 213 | } 214 | 215 | guard textField?.text?.isEmpty == false else { 216 | Drop.down("Please, enter a text", state: .warning) 217 | return 218 | } 219 | 220 | startCrawling() 221 | 222 | let textFieldText = textField?.text ?? String() 223 | 224 | if let url = slp.extractURL(text: textFieldText), 225 | let cached = slp.cache.slp_getCachedResponse(url: url.absoluteString) { 226 | result = cached 227 | setData() 228 | 229 | printResult(result) 230 | } else { 231 | slp.preview( 232 | textFieldText, 233 | onSuccess: { result in 234 | printResult(result) 235 | 236 | self.result = result 237 | self.setData() 238 | }, 239 | onError: { error in 240 | print(error) 241 | self.endCrawling() 242 | 243 | Drop.down(error.description, state: .error) 244 | } 245 | ) 246 | } 247 | } 248 | 249 | @IBAction func openWithAction(_ sender: UIButton) { 250 | if let url = result.finalUrl { 251 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 252 | } 253 | } 254 | } 255 | 256 | // MARK: - UITextFieldDelegate 257 | 258 | extension ViewController: UITextFieldDelegate { 259 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 260 | submitAction(textField) 261 | self.textField?.resignFirstResponder() 262 | return true 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | #### 4.x Releases 4 | - `4.0.x` Releases - [4.0.0](#400) 5 | 6 | #### 3.x Releases 7 | - `3.4.x` Releases - [3.4.0](#340) 8 | - `3.3.x` Releases - [3.3.0](#330) 9 | - `3.2.x` Releases - [3.2.0](#320) 10 | - `3.1.x` Releases - [3.1.0](#310) 11 | - `3.0.x` Releases - [3.0.0](#300) | [3.0.1](#301) 12 | 13 | #### 2.x Releases 14 | - `2.3.x` Releases - [2.3.0](#230) | [2.3.1](#231) 15 | - `2.2.x` Releases - [2.2.0](#220) 16 | - `2.1.x` Releases - [2.1.0](#210) 17 | - `2.0.x` Releases - [2.0.0](#200) | [2.0.1](#201) | [2.0.2](#202) | [2.0.3](#203) | [2.0.4](#204) | [2.0.5](#205) | [2.0.6](#206) | [2.0.7](#207) 18 | 19 | #### 1.x Releases 20 | - `1.0.x` Releases - [1.0.0](#100) | [1.0.1](#101) 21 | 22 | #### 0.x Releases 23 | - `0.1.x` Releases - [0.1.0](#010) | [0.1.1](#011) | [0.1.2](#012) | [0.1.3](#013) | [0.1.4](#014) | [0.1.5](#015) | [0.1.6](#016) 24 | - `0.0.x` Releases - [0.0.2](#002) | [0.0.3](#003) 25 | 26 | --- 27 | 28 | ## [4.0.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/4.0.0) 29 | 30 | #### Added 31 | - Annotated `Cancelable.cancel()` as `@objc` to make it compatibale with Objective-C [#135](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/135) 32 | - Added removal of duplicates from the images array [#45](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/45) 33 | - Added capture of base URL [#45](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/45) 34 | - Changed by [LeonardoCardoso](https://github.com/LeonardoCardoso) 35 | - Make text kLimit to public variable (make user can config limit) 36 | - Changed by [zhocker](https://github.com/zhocker) 37 | 38 | #### Changed 39 | - Updated Package.swift to swift-tools-version:5.0 40 | - Changed by [LeonardoCardoso](https://github.com/LeonardoCardoso) 41 | - Updated README.md [#150](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/150) 42 | - Changed by [benlmyers](https://github.com/benlmyers) 43 | - Updated regex limit [#148](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/148) 44 | - Changed by [kinhvodoi92](https://github.com/kinhvodoi92) 45 | - Modernized code 46 | - Changed by [LeonardoCardoso](https://github.com/LeonardoCardoso) 47 | - Mark public structs Sendable and add an async preview function 48 | - Changed by [harlanhaskins](https://github.com/harlanhaskins) 49 | 50 | #### Removed 51 | - Removed VALID_ARCHS from xcodeproj [#154](https://github.com/LeonardoCardoso/SwiftLinkPreview/pull/154) 52 | - Changed by [eliburke](https://github.com/eliburke) 53 | 54 | ### Fixed 55 | - Amazon links did not contain og tags with the default user agent [#154](https://github.com/LeonardoCardoso/SwiftLinkPreview/pull/155) 56 | - Changed by [chadpav](https://github.com/chadpav) 57 | 58 | 59 | ## [3.4.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/3.4.0) 60 | 61 | #### Added 62 | - Added support for m3u8 lists [#138](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/138) 63 | - Added by [jeffhodsdon](https://github.com/jeffhodsdon) 64 | 65 | #### Changed 66 | - Resolve relative image URLs against the request URL. [#136](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/136) 67 | - Changed by [lhunath](https://github.com/lhunath) 68 | - Video parsing fix [#138](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/138) 69 | - Changed by [jeffhodsdon](https://github.com/jeffhodsdon) 70 | - Fixed github link image for `og:image` property. [#145](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/145) 71 | - Changed by [MuhtasimTanmoy](https://github.com/MuhtasimTanmoy/) 72 | 73 | ## [3.3.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/3.3.0) 74 | 75 | #### Changed 76 | - Handle empty landing pages with HTML meta redirects 77 | - Changed by [lhunath](https://github.com/lhunath) 78 | - fixed youtube and open graph tags image metadata issues 79 | - Changed by [nafis042](https://github.com/nafis042) 80 | - Fixed github link image for `og:image` property. 81 | - Changed by [MuhtasimTanmoy](https://github.com/MuhtasimTanmoy/) 82 | 83 | ## [3.2.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/3.2.0) 84 | 85 | #### Changed 86 | - Updated Package.swift to swift-tools-version:4.2 87 | - Changed by [skunkworker](https://github.com/skunkworker) 88 | - Fixes the NSRange to use a String defined range instead of inferring length from the count property 89 | - Changed by [adamwulf](https://github.com/adamwulf) 90 | 91 | 92 | ## [3.1.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/3.1.0) 93 | 94 | #### Changed 95 | 96 | - Swift 5 compatibility 97 | - Added by [Rajesh](https://github.com/rajeshbeats) 98 | - Fixed lack case nil when unwrap data 99 | - Added by [Quang Truong Tuan](https://github.com/tuanquanghpvn) 100 | 101 | ## [3.0.1](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/3.0.1) 102 | 103 | #### Added 104 | 105 | - Added schema.org price crawler 106 | - Added by [William Gossard](https://github.com/nova974) 107 | 108 | 109 | ## [3.0.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/3.0.0) 110 | Released on 2019-01-02. 111 | 112 | #### Added 113 | 114 | - Added isVideo() check 115 | - Added by [Onur Genes](https://github.com/onurgenes) 116 | 117 | #### Changed 118 | - Response is now a swift `struct` instead of a `Dictionary` 119 | - Moving to Swift 4.2 120 | - Changed by [Giuseppe Travasoni](https://github.com/neobeppe) 121 | 122 | ## [2.3.1](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.3.1) 123 | Released on 2018-09-23. 124 | 125 | #### Changed 126 | 127 | - Fixed missing Accept header 128 | - Changed by [Maarten Billemont](https://github.com/lhunath) 129 | - Example with Cocoapods 130 | - Changed by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 131 | 132 | ## [2.3.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.3.0) 133 | Released on 2018-06-07. 134 | 135 | #### Changed 136 | 137 | - Improved scraping - issues [#70](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/70) [#86](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/86) 138 | - Handle in-url redirections - issue [#83](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/83) 139 | - Swift 4.2 Compatibility - issue [#85](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/85) 140 | - Changed by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 141 | 142 | ## [2.2.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.2.0) 143 | Released on 2018-01-06. 144 | 145 | #### Changed 146 | 147 | - Improved scraping 148 | - Improved error logging 149 | - Retry as GET if HEAD is redirected 150 | - String charset set to UTF16 151 | - No force wrapping 152 | - Changed by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 153 | 154 | ## [2.1.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.1.0) 155 | Released on 2017-11-09. 156 | 157 | #### Added 158 | 159 | - Swift 4 - pr [#67](https://github.com/LeonardoCardoso/SwiftLinkPreview/pull/67). 160 | - Added by [Stephen Hayes](https://github.com/schayes04). 161 | 162 | ## [2.0.7](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.0.7) 163 | Released on 2017-07-24. 164 | 165 | #### Added 166 | 167 | - `Extract icon` - pr [#60](https://github.com/LeonardoCardoso/SwiftLinkPreview/pull/60). 168 | - Added by [Vincent Toms](https://github.com/vinnyt). 169 | 170 | ## [2.0.6](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.0.6) 171 | Released on 2017-07-06. 172 | 173 | #### Changed 174 | 175 | - `NSDataDetector` - pr [#56](https://github.com/LeonardoCardoso/SwiftLinkPreview/pull/56). 176 | - Changed by [Vincent Toms](https://github.com/vinnyt). 177 | 178 | ## [2.0.5](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.0.5) 179 | Released on 2017-06-08. 180 | 181 | #### Changed 182 | 183 | - `extractURL` made public - issue [#52](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/52). 184 | - Changed by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 185 | 186 | ## [2.0.4](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.0.4) 187 | Released on 2017-04-15. 188 | 189 | #### Changed 190 | 191 | - Whitespace and new lines - issue [#47](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/47). 192 | - Support for macOS 10.10 - issue [#48](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/48). 193 | - Changed by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 194 | 195 | ## [2.0.3](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.0.3) 196 | Released on 2017-03-13. 197 | 198 | #### Changed 199 | 200 | - Renamed the Objective-C compatible wrapper fro the preview method to previewLink. This resolves ambiguous method errors in Swift builds - issue [#41](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/41). 201 | - Changed by [David Gifford](https://github.com/giffnyc). 202 | 203 | ## [2.0.2](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.0.2) 204 | Released on 2017-03-09. 205 | 206 | #### Added 207 | - Objective-C init method with no parameters, defaults to the same options as the Swift default parameters. 208 | - Objective-C init method which allows user to set parameters - passing nil will default the parameters. InMemoryCache is a BOOL parameter to use or not use a cache. 209 | - Objective-C preview method which returns a dictionary of values on success, and an NSError object on failure which contains a localized error description. 210 | - Added by [David Gifford](https://github.com/giffnyc). 211 | 212 | #### Changed 213 | - Referenced objects are now derived from NSObject to make them Objective-C compatible. 214 | - Changed by [David Gifford](https://github.com/giffnyc). 215 | 216 | ## [2.0.1](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.0.1) 217 | Released on 2017-02-24. 218 | 219 | #### Changed 220 | - Local analysis out of threads. 221 | - iOS8 backport compatibility 222 | - Changed by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 223 | 224 | #### Fixed 225 | - Crash when no URL was sent to SLP. 226 | - Fixed by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 227 | 228 | ## [2.0.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/2.0.0) 229 | Released on 2017-01-19. 230 | 231 | #### Changed 232 | - Fully asynchronous (DispatchQueue). 233 | - Removed global state 234 | - Better response dictionary subscription via Enum 235 | - Configurable via constructor (URLSession, Work Queue, Response Queue) 236 | - Changed by [Yehor Popovych](https://github.com/ypopovych). 237 | 238 | #### Added 239 | - Caching support (InMemoryCache, but can be extended to other types) 240 | - Added by [Yehor Popovych](https://github.com/ypopovych). 241 | 242 | #### API breaking changes 243 | - Subscriptions via Enum require changes in current code 244 | - `preview` method returns a Cancellable object with cancel method. This allows reusing of single configured SLP instance for multiple requests. cancel method removed from SwiftLinkPreview class. 245 | 246 |


247 | 248 | ## [1.0.1](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/1.0.1) 249 | Released on 2016-09-17. 250 | 251 | #### Added 252 | - Compatibility with Obj-C. 253 | - Added by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 254 | 255 | ## [1.0.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/1.0.0) 256 | Released on 2016-09-14. 257 | 258 | #### Added 259 | - Major Version 1.0.0 with Swift 3.0. 260 | - Added by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 261 | 262 |


263 | 264 | ## [0.1.6](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/0.1.6) 265 | Released on 2016-09-04. 266 | 267 | #### Fixed 268 | - PreviewError conformance to ErrorType. 269 | - Fixed by [Daniel Rhodes](https://github.com/danielrhodes). 270 | 271 | ## [0.1.5](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/0.1.5) 272 | Released on 2016-07-19. 273 | 274 | #### Added 275 | - Background task. 276 | - Added by [Fraser](https://github.com/fraserscottmorrison). 277 | 278 | #### Removed 279 | - Removed resources from podspec file. 280 | - Removed by [Fraser](https://github.com/fraserscottmorrison). 281 | 282 | ## [0.1.4](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/0.1.4) 283 | Released on 2016-07-19. 284 | 285 | #### Added 286 | - Improved crawling. `itemprop` is now a supported meta property. 287 | - More tests. 288 | - Added by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 289 | 290 | #### Removed 291 | - Removed unnecessary data from the code such as `style`, `link`, `comments` and `script` for faster analysis. Additionally I've put it inside a `dispatch_async`. [#19](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/19) 292 | - Removed by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 293 | 294 | ## [0.1.3](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/0.1.3) 295 | Released on 2016-07-16. 296 | 297 | #### Added 298 | - Improved URL parsing. No need to add `http://` or `https://` in the start of an URL anymore. [#17](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/17) 299 | - URL parsing tests. 300 | - Tests for other platforms. 301 | - Added by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 302 | 303 | #### Fixed 304 | - Image url uses finalUrl host component rather than its full path 305 | - Fixed by [Fraser](https://github.com/fraserscottmorrison) 306 | 307 | 308 | ## [0.1.2](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/0.1.2) 309 | Released on 2016-07-13. 310 | 311 | #### Added 312 | - Improved way to get the info. [#13](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/13) 313 | - Added by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 314 | 315 | #### Removed 316 | - Remove Package.swift from .xcproject to avoid build issues. Still supporting SPM though. [#16](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/16) 317 | - Added by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 318 | 319 | ## [0.1.1](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/0.1.1) 320 | Released on 2016-07-11. 321 | 322 | #### Added 323 | - Tests. [#1](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/1) 324 | - CI. [#5](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/5) 325 | - Added by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 326 | 327 | #### Fixed 328 | - Crash when parsing pages without title - [#11](https://github.com/LeonardoCardoso/SwiftLinkPreview/issues/11) 329 | - Fixed by [Fraser](https://github.com/fraserscottmorrison) 330 | 331 | ## [0.1.0](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/0.1.0) 332 | Released on 2016-07-04. 333 | 334 | #### Added 335 | - Support for Swift Package Manager. [#3](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/3) [#8](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/8) 336 | - Support for macOS, watchOS, tvOS. [#9](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/9) 337 | - Added by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 338 | 339 | ## [0.0.3](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/0.0.3) 340 | Released on 2016-06-25. 341 | 342 | #### Added 343 | - Support for Carthage. [#3](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/3) [#7](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/7) 344 | - GitHub configs 345 | - Added by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 346 | 347 | #### Removed 348 | - Alamofire. [#6](https://github.com/LeonardoCardoso/Swift-Link-Preview/issues/6) 349 | - Removed by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 350 | 351 | 352 | ## [0.0.2](https://github.com/LeonardoCardoso/Swift-Link-Preview/releases/tag/0.0.2) 353 | Released on 2016-06-23. 354 | 355 | #### Added 356 | - Initial release of SwiftLinkPreview. 357 | - Added by [Leonardo Cardoso](https://github.com/LeonardoCardoso). 358 | -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 585769D386440A5828F1C5BF /* Pods_SwiftLinkPreviewExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92798956BA5AF1F108A1DA5B /* Pods_SwiftLinkPreviewExample.framework */; }; 11 | 984816501D89968600CD6350 /* AlamofireSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9848164F1D89968600CD6350 /* AlamofireSource.swift */; }; 12 | 98846C7B1D09AA1C00846726 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98846C7A1D09AA1C00846726 /* AppDelegate.swift */; }; 13 | 98846C7D1D09AA1C00846726 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98846C7C1D09AA1C00846726 /* ViewController.swift */; }; 14 | 98846C801D09AA1C00846726 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 98846C7E1D09AA1C00846726 /* Main.storyboard */; }; 15 | 98846C821D09AA1C00846726 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 98846C811D09AA1C00846726 /* Assets.xcassets */; }; 16 | 98846C851D09AA1C00846726 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 98846C831D09AA1C00846726 /* LaunchScreen.storyboard */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 92798956BA5AF1F108A1DA5B /* Pods_SwiftLinkPreviewExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftLinkPreviewExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 9848164F1D89968600CD6350 /* AlamofireSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlamofireSource.swift; sourceTree = ""; }; 22 | 98846C771D09AA1B00846726 /* SwiftLinkPreviewExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftLinkPreviewExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 98846C7A1D09AA1C00846726 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 98846C7C1D09AA1C00846726 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | 98846C7F1D09AA1C00846726 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | 98846C811D09AA1C00846726 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | 98846C841D09AA1C00846726 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | 98846C861D09AA1C00846726 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | AD5D997013810B9AD1EC5F66 /* Pods-SwiftLinkPreviewExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftLinkPreviewExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftLinkPreviewExample/Pods-SwiftLinkPreviewExample.release.xcconfig"; sourceTree = ""; }; 30 | DC5AD87FE707BBB1FB4D28C9 /* Pods-SwiftLinkPreviewExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftLinkPreviewExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftLinkPreviewExample/Pods-SwiftLinkPreviewExample.debug.xcconfig"; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 98846C741D09AA1B00846726 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | 585769D386440A5828F1C5BF /* Pods_SwiftLinkPreviewExample.framework in Frameworks */, 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | 7FC2CF9198B8897DA339C1A2 /* Pods */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | DC5AD87FE707BBB1FB4D28C9 /* Pods-SwiftLinkPreviewExample.debug.xcconfig */, 49 | AD5D997013810B9AD1EC5F66 /* Pods-SwiftLinkPreviewExample.release.xcconfig */, 50 | ); 51 | name = Pods; 52 | sourceTree = ""; 53 | }; 54 | 98348AB41D09B4E2003FA2B3 /* Delegates */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 98846C7A1D09AA1C00846726 /* AppDelegate.swift */, 58 | ); 59 | path = Delegates; 60 | sourceTree = ""; 61 | }; 62 | 98348AB51D09B4E8003FA2B3 /* Controllers */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 98846C7C1D09AA1C00846726 /* ViewController.swift */, 66 | ); 67 | path = Controllers; 68 | sourceTree = ""; 69 | }; 70 | 98348AB61D09B4EF003FA2B3 /* Storyboards */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 98846C831D09AA1C00846726 /* LaunchScreen.storyboard */, 74 | 98846C7E1D09AA1C00846726 /* Main.storyboard */, 75 | ); 76 | path = Storyboards; 77 | sourceTree = ""; 78 | }; 79 | 9848164E1D89967300CD6350 /* ThirdParty */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 9848164F1D89968600CD6350 /* AlamofireSource.swift */, 83 | ); 84 | name = ThirdParty; 85 | sourceTree = ""; 86 | }; 87 | 98846C6E1D09AA1B00846726 = { 88 | isa = PBXGroup; 89 | children = ( 90 | 98846C781D09AA1B00846726 /* Products */, 91 | 98846C791D09AA1B00846726 /* SwiftLinkPreviewExample */, 92 | 7FC2CF9198B8897DA339C1A2 /* Pods */, 93 | EDC6F598637249A29281705D /* Frameworks */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | 98846C781D09AA1B00846726 /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 98846C771D09AA1B00846726 /* SwiftLinkPreviewExample.app */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | 98846C791D09AA1B00846726 /* SwiftLinkPreviewExample */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 9848164E1D89967300CD6350 /* ThirdParty */, 109 | 98348AB51D09B4E8003FA2B3 /* Controllers */, 110 | 98348AB41D09B4E2003FA2B3 /* Delegates */, 111 | 98348AB61D09B4EF003FA2B3 /* Storyboards */, 112 | 98846C811D09AA1C00846726 /* Assets.xcassets */, 113 | 98846C861D09AA1C00846726 /* Info.plist */, 114 | ); 115 | path = SwiftLinkPreviewExample; 116 | sourceTree = ""; 117 | }; 118 | EDC6F598637249A29281705D /* Frameworks */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 92798956BA5AF1F108A1DA5B /* Pods_SwiftLinkPreviewExample.framework */, 122 | ); 123 | name = Frameworks; 124 | sourceTree = ""; 125 | }; 126 | /* End PBXGroup section */ 127 | 128 | /* Begin PBXNativeTarget section */ 129 | 98846C761D09AA1B00846726 /* SwiftLinkPreviewExample */ = { 130 | isa = PBXNativeTarget; 131 | buildConfigurationList = 98846C891D09AA1C00846726 /* Build configuration list for PBXNativeTarget "SwiftLinkPreviewExample" */; 132 | buildPhases = ( 133 | 2502A4C17D8C620A71C1E301 /* [CP] Check Pods Manifest.lock */, 134 | 98846C731D09AA1B00846726 /* Sources */, 135 | 98846C741D09AA1B00846726 /* Frameworks */, 136 | 98846C751D09AA1B00846726 /* Resources */, 137 | 4BA4E1147F2DA431F61F58A7 /* [CP] Embed Pods Frameworks */, 138 | ); 139 | buildRules = ( 140 | ); 141 | dependencies = ( 142 | ); 143 | name = SwiftLinkPreviewExample; 144 | productName = SwiftLinkPreviewExample; 145 | productReference = 98846C771D09AA1B00846726 /* SwiftLinkPreviewExample.app */; 146 | productType = "com.apple.product-type.application"; 147 | }; 148 | /* End PBXNativeTarget section */ 149 | 150 | /* Begin PBXProject section */ 151 | 98846C6F1D09AA1B00846726 /* Project object */ = { 152 | isa = PBXProject; 153 | attributes = { 154 | LastSwiftUpdateCheck = 0730; 155 | LastUpgradeCheck = 1240; 156 | ORGANIZATIONNAME = leocardz.com; 157 | TargetAttributes = { 158 | 98846C761D09AA1B00846726 = { 159 | CreatedOnToolsVersion = 7.3; 160 | LastSwiftMigration = 1240; 161 | ProvisioningStyle = Automatic; 162 | }; 163 | }; 164 | }; 165 | buildConfigurationList = 98846C721D09AA1B00846726 /* Build configuration list for PBXProject "SwiftLinkPreviewExample" */; 166 | compatibilityVersion = "Xcode 8.0"; 167 | developmentRegion = en; 168 | hasScannedForEncodings = 0; 169 | knownRegions = ( 170 | en, 171 | Base, 172 | ); 173 | mainGroup = 98846C6E1D09AA1B00846726; 174 | productRefGroup = 98846C781D09AA1B00846726 /* Products */; 175 | projectDirPath = ""; 176 | projectRoot = ""; 177 | targets = ( 178 | 98846C761D09AA1B00846726 /* SwiftLinkPreviewExample */, 179 | ); 180 | }; 181 | /* End PBXProject section */ 182 | 183 | /* Begin PBXResourcesBuildPhase section */ 184 | 98846C751D09AA1B00846726 /* Resources */ = { 185 | isa = PBXResourcesBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | 98846C851D09AA1C00846726 /* LaunchScreen.storyboard in Resources */, 189 | 98846C821D09AA1C00846726 /* Assets.xcassets in Resources */, 190 | 98846C801D09AA1C00846726 /* Main.storyboard in Resources */, 191 | ); 192 | runOnlyForDeploymentPostprocessing = 0; 193 | }; 194 | /* End PBXResourcesBuildPhase section */ 195 | 196 | /* Begin PBXShellScriptBuildPhase section */ 197 | 2502A4C17D8C620A71C1E301 /* [CP] Check Pods Manifest.lock */ = { 198 | isa = PBXShellScriptBuildPhase; 199 | buildActionMask = 2147483647; 200 | files = ( 201 | ); 202 | inputFileListPaths = ( 203 | ); 204 | inputPaths = ( 205 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 206 | "${PODS_ROOT}/Manifest.lock", 207 | ); 208 | name = "[CP] Check Pods Manifest.lock"; 209 | outputFileListPaths = ( 210 | ); 211 | outputPaths = ( 212 | "$(DERIVED_FILE_DIR)/Pods-SwiftLinkPreviewExample-checkManifestLockResult.txt", 213 | ); 214 | runOnlyForDeploymentPostprocessing = 0; 215 | shellPath = /bin/sh; 216 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 217 | showEnvVarsInLog = 0; 218 | }; 219 | 4BA4E1147F2DA431F61F58A7 /* [CP] Embed Pods Frameworks */ = { 220 | isa = PBXShellScriptBuildPhase; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | ); 224 | inputPaths = ( 225 | "${PODS_ROOT}/Target Support Files/Pods-SwiftLinkPreviewExample/Pods-SwiftLinkPreviewExample-frameworks.sh", 226 | "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", 227 | "${BUILT_PRODUCTS_DIR}/AlamofireImage/AlamofireImage.framework", 228 | "${BUILT_PRODUCTS_DIR}/ImageSlideshow/ImageSlideshow.framework", 229 | "${BUILT_PRODUCTS_DIR}/SwiftLinkPreview/SwiftLinkPreview.framework", 230 | "${BUILT_PRODUCTS_DIR}/SwiftyDrop/SwiftyDrop.framework", 231 | ); 232 | name = "[CP] Embed Pods Frameworks"; 233 | outputPaths = ( 234 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", 235 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AlamofireImage.framework", 236 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ImageSlideshow.framework", 237 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftLinkPreview.framework", 238 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyDrop.framework", 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | shellPath = /bin/sh; 242 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SwiftLinkPreviewExample/Pods-SwiftLinkPreviewExample-frameworks.sh\"\n"; 243 | showEnvVarsInLog = 0; 244 | }; 245 | /* End PBXShellScriptBuildPhase section */ 246 | 247 | /* Begin PBXSourcesBuildPhase section */ 248 | 98846C731D09AA1B00846726 /* Sources */ = { 249 | isa = PBXSourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | 98846C7D1D09AA1C00846726 /* ViewController.swift in Sources */, 253 | 98846C7B1D09AA1C00846726 /* AppDelegate.swift in Sources */, 254 | 984816501D89968600CD6350 /* AlamofireSource.swift in Sources */, 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | /* End PBXSourcesBuildPhase section */ 259 | 260 | /* Begin PBXVariantGroup section */ 261 | 98846C7E1D09AA1C00846726 /* Main.storyboard */ = { 262 | isa = PBXVariantGroup; 263 | children = ( 264 | 98846C7F1D09AA1C00846726 /* Base */, 265 | ); 266 | name = Main.storyboard; 267 | path = .; 268 | sourceTree = ""; 269 | }; 270 | 98846C831D09AA1C00846726 /* LaunchScreen.storyboard */ = { 271 | isa = PBXVariantGroup; 272 | children = ( 273 | 98846C841D09AA1C00846726 /* Base */, 274 | ); 275 | name = LaunchScreen.storyboard; 276 | path = .; 277 | sourceTree = ""; 278 | }; 279 | /* End PBXVariantGroup section */ 280 | 281 | /* Begin XCBuildConfiguration section */ 282 | 98846C871D09AA1C00846726 /* Debug */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | ALWAYS_SEARCH_USER_PATHS = NO; 286 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 287 | CLANG_ANALYZER_NONNULL = YES; 288 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 289 | CLANG_CXX_LIBRARY = "libc++"; 290 | CLANG_ENABLE_MODULES = YES; 291 | CLANG_ENABLE_OBJC_ARC = YES; 292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_COMMA = YES; 295 | CLANG_WARN_CONSTANT_CONVERSION = YES; 296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_EMPTY_BODY = YES; 299 | CLANG_WARN_ENUM_CONVERSION = YES; 300 | CLANG_WARN_INFINITE_RECURSION = YES; 301 | CLANG_WARN_INT_CONVERSION = YES; 302 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 303 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 304 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 305 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 306 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 307 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 308 | CLANG_WARN_STRICT_PROTOTYPES = YES; 309 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 310 | CLANG_WARN_UNREACHABLE_CODE = YES; 311 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 312 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 313 | COPY_PHASE_STRIP = NO; 314 | DEBUG_INFORMATION_FORMAT = dwarf; 315 | ENABLE_STRICT_OBJC_MSGSEND = YES; 316 | ENABLE_TESTABILITY = YES; 317 | GCC_C_LANGUAGE_STANDARD = gnu99; 318 | GCC_DYNAMIC_NO_PIC = NO; 319 | GCC_NO_COMMON_BLOCKS = YES; 320 | GCC_OPTIMIZATION_LEVEL = 0; 321 | GCC_PREPROCESSOR_DEFINITIONS = ( 322 | "DEBUG=1", 323 | "$(inherited)", 324 | ); 325 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 326 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 327 | GCC_WARN_UNDECLARED_SELECTOR = YES; 328 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 329 | GCC_WARN_UNUSED_FUNCTION = YES; 330 | GCC_WARN_UNUSED_VARIABLE = YES; 331 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 332 | MTL_ENABLE_DEBUG_INFO = YES; 333 | ONLY_ACTIVE_ARCH = YES; 334 | SDKROOT = iphoneos; 335 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 336 | SWIFT_VERSION = 5.0; 337 | TARGETED_DEVICE_FAMILY = "1,2"; 338 | }; 339 | name = Debug; 340 | }; 341 | 98846C881D09AA1C00846726 /* Release */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ALWAYS_SEARCH_USER_PATHS = NO; 345 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 346 | CLANG_ANALYZER_NONNULL = YES; 347 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 348 | CLANG_CXX_LIBRARY = "libc++"; 349 | CLANG_ENABLE_MODULES = YES; 350 | CLANG_ENABLE_OBJC_ARC = YES; 351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_COMMA = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 357 | CLANG_WARN_EMPTY_BODY = YES; 358 | CLANG_WARN_ENUM_CONVERSION = YES; 359 | CLANG_WARN_INFINITE_RECURSION = YES; 360 | CLANG_WARN_INT_CONVERSION = YES; 361 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 362 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 363 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 364 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 365 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 366 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 367 | CLANG_WARN_STRICT_PROTOTYPES = YES; 368 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 369 | CLANG_WARN_UNREACHABLE_CODE = YES; 370 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 371 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 372 | COPY_PHASE_STRIP = NO; 373 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 374 | ENABLE_NS_ASSERTIONS = NO; 375 | ENABLE_STRICT_OBJC_MSGSEND = YES; 376 | GCC_C_LANGUAGE_STANDARD = gnu99; 377 | GCC_NO_COMMON_BLOCKS = YES; 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 = 13.0; 385 | MTL_ENABLE_DEBUG_INFO = NO; 386 | SDKROOT = iphoneos; 387 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 388 | SWIFT_VERSION = 5.0; 389 | TARGETED_DEVICE_FAMILY = "1,2"; 390 | VALIDATE_PRODUCT = YES; 391 | }; 392 | name = Release; 393 | }; 394 | 98846C8A1D09AA1C00846726 /* Debug */ = { 395 | isa = XCBuildConfiguration; 396 | baseConfigurationReference = DC5AD87FE707BBB1FB4D28C9 /* Pods-SwiftLinkPreviewExample.debug.xcconfig */; 397 | buildSettings = { 398 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 399 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 400 | DEVELOPMENT_TEAM = ""; 401 | INFOPLIST_FILE = SwiftLinkPreviewExample/Info.plist; 402 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 403 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 404 | PRODUCT_BUNDLE_IDENTIFIER = com.leocardz.SLPExample; 405 | PRODUCT_NAME = "$(TARGET_NAME)"; 406 | SWIFT_VERSION = 5.0; 407 | }; 408 | name = Debug; 409 | }; 410 | 98846C8B1D09AA1C00846726 /* Release */ = { 411 | isa = XCBuildConfiguration; 412 | baseConfigurationReference = AD5D997013810B9AD1EC5F66 /* Pods-SwiftLinkPreviewExample.release.xcconfig */; 413 | buildSettings = { 414 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 415 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 416 | DEVELOPMENT_TEAM = ""; 417 | INFOPLIST_FILE = SwiftLinkPreviewExample/Info.plist; 418 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 419 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 420 | PRODUCT_BUNDLE_IDENTIFIER = com.leocardz.SLPExample; 421 | PRODUCT_NAME = "$(TARGET_NAME)"; 422 | SWIFT_VERSION = 5.0; 423 | }; 424 | name = Release; 425 | }; 426 | /* End XCBuildConfiguration section */ 427 | 428 | /* Begin XCConfigurationList section */ 429 | 98846C721D09AA1B00846726 /* Build configuration list for PBXProject "SwiftLinkPreviewExample" */ = { 430 | isa = XCConfigurationList; 431 | buildConfigurations = ( 432 | 98846C871D09AA1C00846726 /* Debug */, 433 | 98846C881D09AA1C00846726 /* Release */, 434 | ); 435 | defaultConfigurationIsVisible = 0; 436 | defaultConfigurationName = Release; 437 | }; 438 | 98846C891D09AA1C00846726 /* Build configuration list for PBXNativeTarget "SwiftLinkPreviewExample" */ = { 439 | isa = XCConfigurationList; 440 | buildConfigurations = ( 441 | 98846C8A1D09AA1C00846726 /* Debug */, 442 | 98846C8B1D09AA1C00846726 /* Release */, 443 | ); 444 | defaultConfigurationIsVisible = 0; 445 | defaultConfigurationName = Release; 446 | }; 447 | /* End XCConfigurationList section */ 448 | }; 449 | rootObject = 98846C6F1D09AA1B00846726 /* Project object */; 450 | } 451 | -------------------------------------------------------------------------------- /Example/SwiftLinkPreviewExample/Storyboards/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 60 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 114 | 123 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 170 | 173 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | --------------------------------------------------------------------------------