├── 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 = "