diff --git a/Example/LinkPreviewKit.xcodeproj/project.pbxproj b/Example/LinkPreviewKit.xcodeproj/project.pbxproj index 5b64251..08923cc 100644 --- a/Example/LinkPreviewKit.xcodeproj/project.pbxproj +++ b/Example/LinkPreviewKit.xcodeproj/project.pbxproj @@ -8,8 +8,10 @@ /* Begin PBXBuildFile section */ 29D20A2022924E0FB787BD00 /* libPods-Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 051A10CA141AB60D6E3601A1 /* libPods-Tests.a */; }; + 458F6AFA1C75B4B100CAAD75 /* t1.html in Resources */ = {isa = PBXBuildFile; fileRef = 458F6AF91C75B4B100CAAD75 /* t1.html */; }; + 45BE8E961C75CA28009E4A0C /* LKTemplateLibraryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 45BE8E951C75CA28009E4A0C /* LKTemplateLibraryTests.m */; }; 45F04C591C747B1700A3FADE /* LKLinkPreviewReaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 45F04C581C747B1700A3FADE /* LKLinkPreviewReaderTests.m */; }; - 45F04C5B1C747BB600A3FADE /* input.html in Resources */ = {isa = PBXBuildFile; fileRef = 45F04C5A1C747BB600A3FADE /* input.html */; }; + 45F04C5B1C747BB600A3FADE /* t0.html in Resources */ = {isa = PBXBuildFile; fileRef = 45F04C5A1C747BB600A3FADE /* t0.html */; }; 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; }; 6003F592195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; @@ -42,8 +44,11 @@ 1B603EBB072AEBA89C02B396 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 2BF196D820679E10C0F41C26 /* Pods-LinkPreviewKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LinkPreviewKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LinkPreviewKit/Pods-LinkPreviewKit.debug.xcconfig"; sourceTree = ""; }; 2F1DF689B0F4F9C2778A816F /* Pods-LinkPreviewKit.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LinkPreviewKit.release.xcconfig"; path = "Pods/Target Support Files/Pods-LinkPreviewKit/Pods-LinkPreviewKit.release.xcconfig"; sourceTree = ""; }; + 458F6AF91C75B4B100CAAD75 /* t1.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = t1.html; sourceTree = ""; }; + 458F6AFB1C75BF2800CAAD75 /* LKTypes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = LKTypes.m; path = ../../Pod/Classes/LKTypes.m; sourceTree = ""; }; + 45BE8E951C75CA28009E4A0C /* LKTemplateLibraryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LKTemplateLibraryTests.m; sourceTree = ""; }; 45F04C581C747B1700A3FADE /* LKLinkPreviewReaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LKLinkPreviewReaderTests.m; sourceTree = ""; }; - 45F04C5A1C747BB600A3FADE /* input.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = input.html; sourceTree = ""; }; + 45F04C5A1C747BB600A3FADE /* t0.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = t0.html; sourceTree = ""; }; 45F04C5C1C74811C00A3FADE /* LKLinkPreviewHTMLReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LKLinkPreviewHTMLReader.h; path = ../../Pod/Classes/LKLinkPreviewHTMLReader.h; sourceTree = ""; }; 45F04C5D1C74811C00A3FADE /* LKLinkPreviewHTMLReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = LKLinkPreviewHTMLReader.m; path = ../../Pod/Classes/LKLinkPreviewHTMLReader.m; sourceTree = ""; }; 45F04C631C74BF0B00A3FADE /* LKTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LKTypes.h; path = ../../Pod/Classes/LKTypes.h; sourceTree = ""; }; @@ -168,7 +173,7 @@ children = ( 6003F5B6195388D20070C39A /* Supporting Files */, 45F04C581C747B1700A3FADE /* LKLinkPreviewReaderTests.m */, - 45F04C5A1C747BB600A3FADE /* input.html */, + 45BE8E951C75CA28009E4A0C /* LKTemplateLibraryTests.m */, ); path = Tests; sourceTree = ""; @@ -176,6 +181,8 @@ 6003F5B6195388D20070C39A /* Supporting Files */ = { isa = PBXGroup; children = ( + 45F04C5A1C747BB600A3FADE /* t0.html */, + 458F6AF91C75B4B100CAAD75 /* t1.html */, 6003F5B7195388D20070C39A /* Tests-Info.plist */, 6003F5B8195388D20070C39A /* InfoPlist.strings */, 606FC2411953D9B200FFA9A0 /* Tests-Prefix.pch */, @@ -197,6 +204,7 @@ isa = PBXGroup; children = ( 45F04C631C74BF0B00A3FADE /* LKTypes.h */, + 458F6AFB1C75BF2800CAAD75 /* LKTypes.m */, BAD3F3301AD922D10022466C /* LKLinkPreview.h */, BAD3F3311AD922D10022466C /* LKLinkPreview.m */, BABCB8281AD9247700AE97E1 /* LKLinkPreviewKit.h */, @@ -330,8 +338,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 458F6AFA1C75B4B100CAAD75 /* t1.html in Resources */, 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */, - 45F04C5B1C747BB600A3FADE /* input.html in Resources */, + 45F04C5B1C747BB600A3FADE /* t0.html in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -445,6 +454,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 45BE8E961C75CA28009E4A0C /* LKTemplateLibraryTests.m in Sources */, 45F04C591C747B1700A3FADE /* LKLinkPreviewReaderTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Example/Tests/LKLinkPreviewReaderTests.m b/Example/Tests/LKLinkPreviewReaderTests.m index 9e42bed..46a9d7c 100644 --- a/Example/Tests/LKLinkPreviewReaderTests.m +++ b/Example/Tests/LKLinkPreviewReaderTests.m @@ -10,43 +10,79 @@ #import #import "LKLinkPreviewHTMLReader.h" +#import "LKLinkPreview.h" + +static NSArray *testFiles = nil; +static NSString *const extension = @"html"; -@interface LKLinkPreviewReaderTests : XCTestCase -@property (nonatomic, copy) NSString *testHTML; +@interface LKLinkPreviewReaderTests : XCTestCase @end @implementation LKLinkPreviewReaderTests -- (void)setUp { +- (void)setUp +{ [super setUp]; - NSBundle *bundle = [NSBundle bundleForClass:[self class]]; - NSString *path = [bundle pathForResource:@"input" ofType:@"html"]; - NSString *html = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; - XCTAssertNotNil(html); - self.testHTML = html; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + testFiles = [NSArray arrayWithObjects:@"t0", @"t1", nil]; + }); } -- (void)tearDown { - [super tearDown]; - - self.testHTML = nil; -} - -- (void)testThatTestHTMLLoadsAndIsParseable { - HTMLDocument *document = [HTMLDocument documentWithString:self.testHTML]; - XCTAssertNotNil(document); - XCTAssertNotNil(document.rootElement); +- (void)testThatReaderFindAnyPreviews +{ + LKLinkPreviewHTMLReader *htmlReader = [LKLinkPreviewHTMLReader new]; + for (NSString *file in testFiles) { + HTMLDocument *document = [self loadTestHTMLDocumentWithName:file extension:extension]; + XCTAssertNotNil(document); + [htmlReader linkPreviewFromHTMLDocument:document completionHandler:^(NSArray *previews, NSError *error) { + XCTAssertTrue(previews.count >= 1); + }]; + } } -- (void)testThatReaderFindPreviews { - HTMLDocument *document = [HTMLDocument documentWithString:self.testHTML]; +- (void)testThatReaderFindOpenGraphPreviews +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"OpenGraph previews found"]; + __block NSUInteger count = 0; + LKLinkPreviewHTMLReader *htmlReader = [LKLinkPreviewHTMLReader new]; - [htmlReader linkPreviewFromHTMLDocument:document completionHandler:^(NSArray *previews, NSError *error) { - XCTAssertTrue(previews.count == 1); + for (NSString *file in testFiles) { + HTMLDocument *document = [self loadTestHTMLDocumentWithName:file extension:extension]; + XCTAssertNotNil(document); + [htmlReader linkPreviewFromHTMLDocument:document completionHandler:^(NSArray *previews, NSError *error) { + for (LKLinkPreview *preview in previews) { + if (preview.kind == LKTemplateKindOpenGraph) { + count += 1; + if (count == testFiles.count) { + [expectation fulfill]; + } + } + } + }]; + } + + [self waitForExpectationsWithTimeout:2.0 handler:^(NSError *error) { + if (error) { + XCTFail(@"Couldnt find all OpenGraph previews"); + } }]; + +} + +#pragma mark Helpers + +- (HTMLDocument *)loadTestHTMLDocumentWithName:(NSString *)name extension:(NSString *)extension +{ + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSString *path = [bundle pathForResource:name ofType:extension]; + NSString *html = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; + HTMLDocument *document = [HTMLDocument documentWithString:html]; + + return document; } @end diff --git a/Example/Tests/LKTemplateLibraryTests.m b/Example/Tests/LKTemplateLibraryTests.m new file mode 100644 index 0000000..00650a4 --- /dev/null +++ b/Example/Tests/LKTemplateLibraryTests.m @@ -0,0 +1,45 @@ +// +// LKTemplateLibraryTests.m +// LinkPreviewKit +// +// Created by Andreas Kompanez on 18/02/16. +// Copyright © 2016 Andreas Kompanez. All rights reserved. +// + +#import + +#import "LKTemplateLibrary.h" + +@interface LKTemplateLibraryTests : XCTestCase + +@property (nonatomic) LKTemplateLibrary *library; + +@end + +@implementation LKTemplateLibraryTests + +- (void)setUp { + [super setUp]; + + self.library = [LKTemplateLibrary new]; +} + +- (void)testThatRegisteredPreviewsAreSame +{ + LKLinkPreview *p0 = [self.library fetchOrRegisterNewLinkPreviewByKind:LKTemplateKindTwitterCard]; + LKLinkPreview *p1 = [self.library fetchOrRegisterNewLinkPreviewByKind:LKTemplateKindTwitterCard]; + XCTAssertEqual(p0, p1); +} + +- (void)testThatResetAndRegisterWorks +{ + XCTAssertTrue(self.library.allPreviews.count == 0); + [self.library fetchOrRegisterNewLinkPreviewByKind:LKTemplateKindStandard]; + [self.library fetchOrRegisterNewLinkPreviewByKind:LKTemplateKindTwitterCard]; + XCTAssertTrue(self.library.allPreviews.count == 2); + [self.library resetRegisteredPreviews]; + XCTAssertTrue(self.library.allPreviews.count == 0); +} + + +@end diff --git a/Example/Tests/input.html b/Example/Tests/t0.html similarity index 100% rename from Example/Tests/input.html rename to Example/Tests/t0.html diff --git a/Example/Tests/t1.html b/Example/Tests/t1.html new file mode 100644 index 0000000..ee22ee8 --- /dev/null +++ b/Example/Tests/t1.html @@ -0,0 +1,64 @@ + + + + + + + Product Hunt + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + diff --git a/Pod/Classes/LKLinkPreview.h b/Pod/Classes/LKLinkPreview.h index 00025ac..3974db3 100644 --- a/Pod/Classes/LKLinkPreview.h +++ b/Pod/Classes/LKLinkPreview.h @@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) LKTemplateKind kind; @property (nonatomic, copy) NSString * _Nullable title; @property (nonatomic, copy) NSString * _Nullable type; +@property (nonatomic, copy) NSString * _Nullable siteName; @property (nonatomic) NSURL * _Nullable URL; @property (nonatomic) NSURL * _Nullable imageURL; @property (nonatomic, copy) NSString * _Nullable linkDescription; diff --git a/Pod/Classes/LKLinkPreview.m b/Pod/Classes/LKLinkPreview.m index 751e0c1..cf9a006 100644 --- a/Pod/Classes/LKLinkPreview.m +++ b/Pod/Classes/LKLinkPreview.m @@ -35,7 +35,7 @@ if ([property hasPrefix:namespace]) { return [property stringByReplacingOccurrencesOfString:namespace withString:@""]; } - return namespace; + return property; } @interface LKLinkPreview () @@ -75,6 +75,9 @@ - (void)setContent:(nullable id)content forProperty:(NSString * _Nonnull)propert else if ([normalized isEqualToString:@"image"]) { self.imageURL = [NSURL URLWithString:content]; } + else if ([normalized isEqualToString:@"site_name"]) { + self.siteName = content; + } else if ([normalized isEqualToString:@"description"]) { self.linkDescription = content; } @@ -83,7 +86,9 @@ - (void)setContent:(nullable id)content forProperty:(NSString * _Nonnull)propert - (NSString *)description { NSMutableString *body = [NSMutableString new]; + [body appendFormat:@"kind: '%@'\n", StringFromLKTemplateKind(self.kind)]; [body appendFormat:@"title: '%@'\n", self.title]; + [body appendFormat:@"siteName: '%@'\n", self.siteName]; [body appendFormat:@"type: '%@'\n", self.type]; [body appendFormat:@"URL: '%@'\n", [self.URL absoluteString]]; [body appendFormat:@"imageURL: '%@'\n", [self.imageURL absoluteString]]; diff --git a/Pod/Classes/LKLinkPreviewHTMLReader.m b/Pod/Classes/LKLinkPreviewHTMLReader.m index 3707250..0482283 100644 --- a/Pod/Classes/LKLinkPreviewHTMLReader.m +++ b/Pod/Classes/LKLinkPreviewHTMLReader.m @@ -14,8 +14,10 @@ #import static NSString *const LKHTMLElementMeta = @"meta"; +static NSString *const LKHTMLElementTitle = @"title"; static NSString *const LKHTMLAttributeContent = @"content"; static NSString *const LKHTMLAttributeProperty = @"property"; +static NSString *const LKHTMLAttributeName = @"name"; @interface LKLinkPreview (LKLinkPreviewHTMLReader) @@ -34,13 +36,13 @@ - (BOOL)isEmpty @interface LKTemplateMatcher : NSObject -- (LKTemplateKind)matchingTemplateByProperty:(NSString *)property; +- (LKTemplateKind)matchingTemplateByKey:(NSString *)property; @end @implementation LKTemplateMatcher -- (LKTemplateKind)matchingTemplateByProperty:(NSString *)property +- (LKTemplateKind)matchingTemplateByKey:(NSString *)property { if ([property hasPrefix:@"og:"]) { return LKTemplateKindOpenGraph; @@ -48,12 +50,61 @@ - (LKTemplateKind)matchingTemplateByProperty:(NSString *)property if ([property hasPrefix:@"twitter:"]) { return LKTemplateKindTwitterCard; } + if ([property isEqualToString:@"description"]) { + return LKTemplateKindStandard; + } return LKTemplateKindUndefined; } @end +@interface LKMetaKeyValuePair : NSObject + +@property (nonatomic, copy) NSString *key; +@property (nonatomic, copy) NSString *value; + +@end + +@implementation LKMetaKeyValuePair + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p> key '%@'; value '%@'", [self class], self, self.key, self.value]; +} + +@end + +@interface LKLKMetaKeyValuePairParser : NSObject + +- (LKMetaKeyValuePair * _Nonnull)parse:(HTMLElement *)element; + +@end + +@implementation LKLKMetaKeyValuePairParser + +- (LKMetaKeyValuePair *)parse:(HTMLElement *)element +{ + NSString *property = [element.attributes objectForKey:LKHTMLAttributeProperty]; + NSString *name = [element.attributes objectForKey:LKHTMLAttributeName]; + NSString *content = [element.attributes objectForKey:LKHTMLAttributeContent]; + NSString *key = nil; + + if (property.length > 0 && name.length == 0) { + key = property; + } + else { + key = name; + } + LKMetaKeyValuePair *pair = [LKMetaKeyValuePair new]; + pair.key = key; + pair.value = content; + + return pair; +} + +@end + @implementation LKLinkPreviewHTMLReader @@ -62,23 +113,29 @@ - (void)linkPreviewFromHTMLDocument:(HTMLDocument *)document completionHandler:( NSArray *metaNodes = [document nodesMatchingSelector:LKHTMLElementMeta]; LKTemplateLibrary *library = [LKTemplateLibrary new]; LKTemplateMatcher *matcher = [LKTemplateMatcher new]; + LKLKMetaKeyValuePairParser *keyValueParser = [LKLKMetaKeyValuePairParser new]; for (id meta in metaNodes) { if (! [meta isKindOfClass:[HTMLElement class]]) { continue; } HTMLElement *metaElement = (HTMLElement *)meta; - NSString *property = [metaElement.attributes objectForKey:LKHTMLAttributeProperty]; - NSString *content = [metaElement.attributes objectForKey:LKHTMLAttributeContent]; - - LKTemplateKind kind = [matcher matchingTemplateByProperty:property]; + LKMetaKeyValuePair *keyValuePair = [keyValueParser parse:metaElement]; + LKTemplateKind kind = [matcher matchingTemplateByKey:keyValuePair.key]; if (kind == LKTemplateKindUndefined) { continue; } LKLinkPreview *preview = [library fetchOrRegisterNewLinkPreviewByKind:kind]; - [preview setContent:content forProperty:property]; + [preview setContent:keyValuePair.value forProperty:keyValuePair.key]; + } + + // Check for Standard Template + LKLinkPreview *standardTemplatePreview = [library fetchOrRegisterNewLinkPreviewByKind:LKTemplateKindStandard]; + if (standardTemplatePreview) { + HTMLElement *titleElement = [document nodesMatchingSelector:LKHTMLElementTitle].firstObject; + standardTemplatePreview.title = titleElement.textContent; } if (handler) { diff --git a/Pod/Classes/LKTemplateLibrary.h b/Pod/Classes/LKTemplateLibrary.h index a2c9589..5dbd761 100644 --- a/Pod/Classes/LKTemplateLibrary.h +++ b/Pod/Classes/LKTemplateLibrary.h @@ -13,9 +13,9 @@ @interface LKTemplateLibrary : NSObject /// Returns all the registered @c LKLinkPreview objects -@property (nonatomic, readonly) NSArray *allPreviews; +@property (nonatomic, readonly) NSArray * _Nonnull allPreviews; -- (LKLinkPreview *)fetchOrRegisterNewLinkPreviewByKind:(LKTemplateKind)kind; +- (LKLinkPreview * _Nullable)fetchOrRegisterNewLinkPreviewByKind:(LKTemplateKind)kind; - (void)resetRegisteredPreviews; diff --git a/Pod/Classes/LKTemplateLibrary.m b/Pod/Classes/LKTemplateLibrary.m index 97df5d5..18a9793 100644 --- a/Pod/Classes/LKTemplateLibrary.m +++ b/Pod/Classes/LKTemplateLibrary.m @@ -38,6 +38,10 @@ - (void)resetRegisteredPreviews - (LKLinkPreview *)fetchOrRegisterNewLinkPreviewByKind:(LKTemplateKind)kind { + if (kind == LKTemplateKindUndefined) { + return nil; + } + NSNumber *key = @(kind); LKLinkPreview *preview = [self.registeredPreviews objectForKey:key]; if (! preview) { diff --git a/Pod/Classes/LKTypes.h b/Pod/Classes/LKTypes.h index 6e991d3..d492549 100644 --- a/Pod/Classes/LKTypes.h +++ b/Pod/Classes/LKTypes.h @@ -8,7 +8,9 @@ typedef NS_ENUM(NSInteger, LKTemplateKind) { LKTemplateKindUndefined, - LKTemplateKindDefault, // and meta description + LKTemplateKindStandard, // <title> and meta description LKTemplateKindTwitterCard, LKTemplateKindOpenGraph }; + +extern NSString *StringFromLKTemplateKind(LKTemplateKind kind); diff --git a/Pod/Classes/LKTypes.m b/Pod/Classes/LKTypes.m new file mode 100644 index 0000000..2c4ea74 --- /dev/null +++ b/Pod/Classes/LKTypes.m @@ -0,0 +1,23 @@ +// +// LKTypes.h +// LinkPreviewKit +// +// Created by Andreas Kompanez on 18/02/16. +// Copyright © 2016 Andreas Kompanez. All rights reserved. +// + +#import "LKTypes.h" + +NSString *StringFromLKTemplateKind(LKTemplateKind kind) +{ + if (kind == LKTemplateKindStandard) { + return @"Standard"; + } + if (kind == LKTemplateKindTwitterCard) { + return @"TwitterCard"; + } + if (kind == LKTemplateKindOpenGraph) { + return @"OpenGraph"; + } + return @"Undefined"; +} diff --git a/README.md b/README.md index 390bc20..05e0ae9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![Platform](https://img.shields.io/cocoapods/p/LinkPreviewKit.svg?style=flat)](http://cocoapods.org/pods/LinkPreviewKit) -µLibrary to fetch the social media meta tag information from a website URL. +µLibrary to fetch the social media meta tag information from a website URL. + +Supports Meta Tags for Standard Template (*title* and *description*), TwitterCard (*twitter:*) and OpenGraph (*og:*). ## Usage