diff --git a/HTMLKit.xcodeproj/project.pbxproj b/HTMLKit.xcodeproj/project.pbxproj index 288d887..543bf53 100644 --- a/HTMLKit.xcodeproj/project.pbxproj +++ b/HTMLKit.xcodeproj/project.pbxproj @@ -51,6 +51,10 @@ 623916C51AC707250066B4FE /* HTMLNodeTreeEnumerator.m in Sources */ = {isa = PBXBuildFile; fileRef = 623916C21AC707250066B4FE /* HTMLNodeTreeEnumerator.m */; }; 623916C71AC7209E0066B4FE /* HTMLKitNodeTreeEnumratorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 623916C61AC7209E0066B4FE /* HTMLKitNodeTreeEnumratorTests.m */; }; 623916C81AC7209E0066B4FE /* HTMLKitNodeTreeEnumratorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 623916C61AC7209E0066B4FE /* HTMLKitNodeTreeEnumratorTests.m */; }; + 6239755A1AC362CA007E26F1 /* HTMLKitTreeConstructionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 623975591AC362CA007E26F1 /* HTMLKitTreeConstructionTests.m */; }; + 6239755B1AC362CA007E26F1 /* HTMLKitTreeConstructionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 623975591AC362CA007E26F1 /* HTMLKitTreeConstructionTests.m */; }; + 6239755E1AC364BB007E26F1 /* HTML5LibTreeConstructionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6239755D1AC364BB007E26F1 /* HTML5LibTreeConstructionTest.m */; }; + 6239755F1AC364BB007E26F1 /* HTML5LibTreeConstructionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6239755D1AC364BB007E26F1 /* HTML5LibTreeConstructionTest.m */; }; 624493A619CCC54100BCDDF4 /* HTMLTokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 624493A419CCC54100BCDDF4 /* HTMLTokenizer.h */; }; 624493A719CCC54100BCDDF4 /* HTMLTokenizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 624493A519CCC54100BCDDF4 /* HTMLTokenizer.m */; }; 624493A819CCC54100BCDDF4 /* HTMLTokenizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 624493A519CCC54100BCDDF4 /* HTMLTokenizer.m */; }; @@ -171,6 +175,9 @@ 623916C11AC707250066B4FE /* HTMLNodeTreeEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLNodeTreeEnumerator.h; sourceTree = ""; }; 623916C21AC707250066B4FE /* HTMLNodeTreeEnumerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLNodeTreeEnumerator.m; sourceTree = ""; }; 623916C61AC7209E0066B4FE /* HTMLKitNodeTreeEnumratorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLKitNodeTreeEnumratorTests.m; sourceTree = ""; }; + 623975591AC362CA007E26F1 /* HTMLKitTreeConstructionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLKitTreeConstructionTests.m; sourceTree = ""; }; + 6239755C1AC364BB007E26F1 /* HTML5LibTreeConstructionTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTML5LibTreeConstructionTest.h; sourceTree = ""; }; + 6239755D1AC364BB007E26F1 /* HTML5LibTreeConstructionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTML5LibTreeConstructionTest.m; sourceTree = ""; }; 624493A419CCC54100BCDDF4 /* HTMLTokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLTokenizer.h; sourceTree = ""; }; 624493A519CCC54100BCDDF4 /* HTMLTokenizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLTokenizer.m; sourceTree = ""; }; 624493A919CCE84A00BCDDF4 /* HTMLTokenizerStates.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HTMLTokenizerStates.h; sourceTree = ""; }; @@ -200,7 +207,7 @@ 625A150619C78ABA00AD0C32 /* HTMLInputStreamReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLInputStreamReader.h; sourceTree = ""; }; 625A150719C78ABA00AD0C32 /* HTMLInputStreamReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLInputStreamReader.m; sourceTree = ""; }; 6279F87119E17DC700F12EE5 /* HTMLParserInsertionModes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HTMLParserInsertionModes.h; sourceTree = ""; }; - 6279F87219E1808D00F12EE5 /* HTMLElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLElement.h; sourceTree = ""; }; + 6279F87219E1808D00F12EE5 /* HTMLElement.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.objc; fileEncoding = 4; path = HTMLElement.h; sourceTree = ""; }; 6279F87319E1808D00F12EE5 /* HTMLElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLElement.m; sourceTree = ""; }; 628B7CE61A080E1000602C87 /* HTMLNamespaces.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HTMLNamespaces.h; sourceTree = ""; }; 62AE593319F97D880043F069 /* HTMLParseErrorToken.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLParseErrorToken.h; sourceTree = ""; }; @@ -276,8 +283,8 @@ 623719441AA1472B002E03C8 /* Nodes */ = { isa = PBXGroup; children = ( - 62362A3F1A9FDE8A00301989 /* HTMLNodes.h */, 623719431AA12EE8002E03C8 /* HTMLQuirksMode.h */, + 62362A3F1A9FDE8A00301989 /* HTMLNodes.h */, 6234584C1A9D2FA4009BD491 /* HTMLNode.h */, 6234584D1A9D2FA4009BD491 /* HTMLNode.m */, 6279F87219E1808D00F12EE5 /* HTMLElement.h */, @@ -296,6 +303,16 @@ name = Nodes; sourceTree = ""; }; + 623975581AC362A5007E26F1 /* Tree Construction */ = { + isa = PBXGroup; + children = ( + 6239755C1AC364BB007E26F1 /* HTML5LibTreeConstructionTest.h */, + 6239755D1AC364BB007E26F1 /* HTML5LibTreeConstructionTest.m */, + 623975591AC362CA007E26F1 /* HTMLKitTreeConstructionTests.m */, + ); + name = "Tree Construction"; + sourceTree = ""; + }; 625A14A219C7829400AD0C32 = { isa = PBXGroup; children = ( @@ -384,6 +401,7 @@ children = ( 623916C61AC7209E0066B4FE /* HTMLKitNodeTreeEnumratorTests.m */, 6236738C1AC0CD2400FF89B3 /* Tokenizer */, + 623975581AC362A5007E26F1 /* Tree Construction */, 625A14CB19C7829400AD0C32 /* Supporting Files */, ); name = Tests; @@ -547,7 +565,7 @@ 625A14A319C7829400AD0C32 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0510; + LastUpgradeCheck = 0610; ORGANIZATIONNAME = BrainCookie; TargetAttributes = { 625A14C219C7829400AD0C32 = { @@ -645,10 +663,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6239755B1AC362CA007E26F1 /* HTMLKitTreeConstructionTests.m in Sources */, 6236738F1AC0CE2500FF89B3 /* HTMLKitTokenizerPerformance.m in Sources */, 624AC90219FBF9ED00BD3C4A /* HTMLKitTokenizerTests.m in Sources */, 623916C81AC7209E0066B4FE /* HTMLKitNodeTreeEnumratorTests.m in Sources */, 62D8345A19FB1AC4009205A9 /* HTML5LibTokenizerTest.m in Sources */, + 6239755F1AC364BB007E26F1 /* HTML5LibTreeConstructionTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -688,10 +708,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6239755A1AC362CA007E26F1 /* HTMLKitTreeConstructionTests.m in Sources */, 6236738E1AC0CE2500FF89B3 /* HTMLKitTokenizerPerformance.m in Sources */, 624AC90119FBF9ED00BD3C4A /* HTMLKitTokenizerTests.m in Sources */, 623916C71AC7209E0066B4FE /* HTMLKitNodeTreeEnumratorTests.m in Sources */, 62D8345919FB1AC4009205A9 /* HTML5LibTokenizerTest.m in Sources */, + 6239755E1AC364BB007E26F1 /* HTML5LibTreeConstructionTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/HTMLKit/HTMLParser.m b/HTMLKit/HTMLParser.m index 1638345..7b227ed 100644 --- a/HTMLKit/HTMLParser.m +++ b/HTMLKit/HTMLParser.m @@ -1941,8 +1941,7 @@ case HTMLTokenTypeStartTag: if ([token.asTagToken.tagName isEqualToAny:@"caption", @"col", @"colgroup", @"tbody", @"td", @"tfoot", @"th", @"thead", @"tr", nil]) { - if (![_stackOfOpenElements hasElementInTableScopeWithTagName:@"td"] && - ![_stackOfOpenElements hasElementInTableScopeWithTagName:@"th"]) { + if (![_stackOfOpenElements hasElementInTableScopeWithAnyOfTagNames:@[@"td", @"th"]]) { [self emitParseError:@"Unexpected Start Tag Token (%@) for element in ", token.asTagToken.tagName]; return; } else { diff --git a/HTMLKitTests/HTML5LibTreeConstructionTest.h b/HTMLKitTests/HTML5LibTreeConstructionTest.h new file mode 100644 index 0000000..7dd5b83 --- /dev/null +++ b/HTMLKitTests/HTML5LibTreeConstructionTest.h @@ -0,0 +1,23 @@ +// +// HTML5LibTreeConstructionTest.h +// HTMLKit +// +// Created by Iska on 25/03/15. +// Copyright (c) 2015 BrainCookie. All rights reserved. +// + +#import + +@class HTMLElement; + +@interface HTML5LibTreeConstructionTest : NSObject + +@property (nonatomic, copy) NSString *testFile; +@property (nonatomic, copy) NSString *data; +@property (nonatomic, strong) NSArray *errors; +@property (nonatomic, strong) HTMLElement *documentFragment; +@property (nonatomic, strong) NSArray *nodes; + ++ (NSDictionary *)loadHTML5LibTreeConstructionTests; + +@end diff --git a/HTMLKitTests/HTML5LibTreeConstructionTest.m b/HTMLKitTests/HTML5LibTreeConstructionTest.m new file mode 100644 index 0000000..21df906 --- /dev/null +++ b/HTMLKitTests/HTML5LibTreeConstructionTest.m @@ -0,0 +1,318 @@ +// +// HTML5LibTreeConstructionTest.m +// HTMLKit +// +// Created by Iska on 25/03/15. +// Copyright (c) 2015 BrainCookie. All rights reserved. +// + +#import "HTML5LibTreeConstructionTest.h" +#import + +#import "HTMLDocumentType.h" +#import "HTMLElement.h" +#import "HTMLText.h" +#import "HTMLComment.h" +#import "HTMLNodeTreeEnumerator.h" + +static NSString * const HTML5LibTests = @"html5lib-tests"; +static NSString * const TreeConstruction = @"tree-construction"; + +@implementation HTML5LibTreeConstructionTest + ++ (NSDictionary *)loadHTML5LibTreeConstructionTests +{ + NSString *path = [[NSBundle bundleForClass:self.class] resourcePath]; + path = [path stringByAppendingPathComponent:HTML5LibTests]; + path = [path stringByAppendingPathComponent:TreeConstruction]; + + NSMutableDictionary *testsMap = [NSMutableDictionary dictionary]; + NSArray *testFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil]; + + for (NSString *testFile in testFiles) { + if (![testFile.pathExtension isEqualToString:@"dat"]) { + continue; + } + + if ([testFile hasPrefix:@"ruby"]) { + // and friends are not yet completely supported + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=26189 + continue; + } + + NSString *testFilePath = [path stringByAppendingPathComponent:testFile]; + NSArray *tests = [HTML5LibTreeConstructionTest loadTestsWithFileAtPath:testFilePath]; + [testsMap setObject:tests forKey:testFile]; + } + + return testsMap; +} + ++ (NSArray *)loadTestsWithFileAtPath:(NSString *)filePath +{ + NSString *contents = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; + + NSMutableArray *tests = [NSMutableArray array]; + + NSScanner *scanner = [NSScanner scannerWithString:contents]; + NSString * (^ nextTest)() = ^ NSString * () { + NSString *str; + [scanner scanUpToString:@"\n\n#data" intoString:&str]; + return str; + }; + + NSRegularExpressionOptions options = NSRegularExpressionDotMatchesLineSeparators| + NSRegularExpressionUseUnixLineSeparators| + NSRegularExpressionAnchorsMatchLines; + + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(^#(.(?!^#))+)" + options:options + error:nil]; + + NSString *rawTest = nil; + while ((rawTest = nextTest()) != nil) { + + HTML5LibTreeConstructionTest *test = [HTML5LibTreeConstructionTest new]; + test.testFile = filePath.lastPathComponent; + + if ([rawTest rangeOfString:@"ruby"].location != NSNotFound) { + // and friends are not yet completely supported + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=26189 + continue; + } + + if ([rawTest rangeOfString:@"template"].location != NSNotFound) { +#warning Implemenet HTML Template + continue; + } + + if ([rawTest rangeOfString:@"#script-off"].location != NSNotFound) { + // Ignore tests for "scripting flag disabled" case + continue; + } + + NSArray *matches = [regex matchesInString:rawTest options:0 range:NSMakeRange(0, rawTest.length)]; + + for (NSTextCheckingResult *result in matches) { + NSString *match = [rawTest substringWithRange:result.range]; + + if ([match hasPrefix:@"#data\n"]) { + NSString *data = [match substringFromIndex:@"#data\n".length]; + if (data.length > 0) { + data = [data substringToIndex:data.length]; + } + test.data = data; + } else if ([match hasPrefix:@"#errors\n"]) { + NSArray *errors = [[match substringFromIndex:@"#errors\n".length] componentsSeparatedByString:@"\n"]; + test.errors = [errors subarrayWithRange:NSMakeRange(0, errors.count)]; + } else if ([match hasPrefix:@"#document-fragment\n"]) { + NSString *fragment = [match substringFromIndex:@"#document-fragment\n".length]; + fragment = [fragment substringToIndex:fragment.length]; + HTMLNamespace namespace = HTMLNamespaceHTML; + if ([fragment hasPrefix:@"math "]) { + fragment = [fragment substringFromIndex:@"math ".length]; + namespace = HTMLNamespaceMathML; + } else if ([fragment hasPrefix:@"svg "]) { + fragment = [fragment substringFromIndex:@"svg ".length]; + namespace = HTMLNamespaceSVG; + } + test.documentFragment = [[HTMLElement alloc] initWithTagName:fragment attributes:nil namespace:namespace]; + } else if ([match hasPrefix:@"#document\n"]) { + NSArray *parts = [[match substringFromIndex:@"#document\n".length] componentsSeparatedByString:@"| "]; + NSArray *nodes = [HTML5LibTreeConstructionTest parseDocument:parts]; + test.nodes = nodes; + } + } + [tests addObject:test]; + } + + return tests; +} + ++ (NSArray *)parseDocument:(NSArray *)parts +{ + NSMutableArray *nodes = [NSMutableArray array]; + NSMutableArray *levels = [NSMutableArray array]; + NSMutableArray *stack = [NSMutableArray array]; + + for (NSString *part in parts) { + if (part.length == 0) { + continue; + } + + NSUInteger level = 0; + NSString *str = parseLevel(part, &level); + + HTMLElement *currentParent = ^ HTMLElement * (NSUInteger childLevel) { + for (NSNumber *level in levels.reverseObjectEnumerator.allObjects) { + if (level.unsignedIntegerValue >= childLevel) { + [levels removeLastObject]; + [stack removeLastObject]; + } + } + return stack.lastObject; + }(level); + + void (^ append)(id ) = ^ (id parsedResult){ + if (currentParent) { + [currentParent appendNode:parsedResult]; + } else { + [nodes addObject:parsedResult]; + } + }; + + id parsedResult = nil; + + if ((parsedResult = parseComment(str))) { + append(parsedResult); + } else if ((parsedResult = parseDocumentType(str))) { + append(parsedResult); + } else if ((parsedResult = parseTag(str))) { + append(parsedResult); + [levels addObject:@(level)]; + [stack addObject:parsedResult]; + } else if ((parsedResult = parseAttribute(str))) { + HTMLElement *element = stack.lastObject; + element[parsedResult[0]] = parsedResult[1]; + } else if ((parsedResult = parseText(str))) { + append(parsedResult); + } + } + + return nodes; +} + +NS_INLINE NSString * parseLevel(NSString *str, NSUInteger *level) +{ + const char *cstr = str.UTF8String; + NSUInteger idx = 0; + while ((*cstr) == ' ') { cstr++; idx++; } + *level = (idx / 2); + return [str substringFromIndex:idx]; +} + +NS_INLINE HTMLDocumentType * parseDocumentType(NSString *str) +{ + if (![str hasPrefix:@" 0) { + NSRange pubRange = [match rangeAtIndex:1]; + if (pubRange.location != NSNotFound) { + publicIdentifier = [rest substringWithRange:NSMakeRange(pubRange.location + 1, pubRange.length - 2)]; + } + + NSRange sysRange = [match rangeAtIndex:2]; + if (sysRange.location != NSNotFound) { + systemIdentifier = [rest substringWithRange:NSMakeRange(sysRange.location + 1, sysRange.length - 2)]; + } + } + + HTMLDocumentType *doctype = [[HTMLDocumentType alloc] initWithName:name + publicIdentifier:publicIdentifier + systemIdentifier:systemIdentifier]; + return doctype; +} + +NS_INLINE HTMLElement * parseTag(NSString *str) +{ + NSRegularExpression *tagRegex = [NSRegularExpression regularExpressionWithPattern:@"^(<.*>)$" + options:NSRegularExpressionAnchorsMatchLines + error:nil]; + if ([tagRegex numberOfMatchesInString:str options:0 range:NSMakeRange(0, str.length)] != 1) { + return nil; + } + + NSTextCheckingResult *match = [tagRegex firstMatchInString:str options:0 range:NSMakeRange(0, str.length)]; + NSRange range = NSMakeRange(match.range.location + 1, match.range.length - 2); + + NSArray *parts = [[str substringWithRange:range] componentsSeparatedByString:@" "]; + NSString *tagName = parts.count == 2 ? parts[1] : parts[0]; + HTMLNamespace namespace = parts.count == 1 ? HTMLNamespaceHTML : ([parts[0] isEqualToString:@"math"] ? HTMLNamespaceMathML : HTMLNamespaceSVG); + + HTMLElement *element = [[HTMLElement alloc] initWithTagName:tagName attributes:nil namespace:namespace]; + return element; +} + +NS_INLINE HTMLText * parseText(NSString *str) +{ + NSRegularExpressionOptions options = NSRegularExpressionDotMatchesLineSeparators| + NSRegularExpressionUseUnixLineSeparators| + NSRegularExpressionAnchorsMatchLines; + + NSRegularExpression *textRegex = [NSRegularExpression regularExpressionWithPattern:@"^(\".*\")" + options:options + error:nil]; + if ([textRegex numberOfMatchesInString:str options:0 range:NSMakeRange(0, str.length)] != 1) { + return nil; + } + + NSTextCheckingResult *match = [textRegex firstMatchInString:str options:0 range:NSMakeRange(0, str.length)]; + NSRange range = NSMakeRange(match.range.location + 1, match.range.length - 2); + HTMLText *text = [[HTMLText alloc] initWithData:[str substringWithRange:range]]; + return text; +} + +NS_INLINE HTMLComment * parseComment(NSString *str) +{ + NSRegularExpressionOptions options = NSRegularExpressionDotMatchesLineSeparators| + NSRegularExpressionUseUnixLineSeparators| + NSRegularExpressionAnchorsMatchLines; + + NSRegularExpression *commentRegex = [NSRegularExpression regularExpressionWithPattern:@"^()$" + options:options + error:nil]; + if ([commentRegex numberOfMatchesInString:str options:0 range:NSMakeRange(0, str.length)] != 1) { + return nil; + } + + NSTextCheckingResult *match = [commentRegex firstMatchInString:str options:0 range:NSMakeRange(0, str.length)]; + NSString *data = [str substringWithRange:match.range]; + data = [data substringWithRange:NSMakeRange(@"".length)]; + HTMLComment *comment = [[HTMLComment alloc] initWithData:data]; + return comment; +} + +NS_INLINE NSArray * parseAttribute(NSString *str) +{ + NSRegularExpressionOptions options = NSRegularExpressionDotMatchesLineSeparators | NSRegularExpressionUseUnixLineSeparators; + + NSRegularExpression *attributeRegex = [NSRegularExpression regularExpressionWithPattern:@"^[^\"](.*=\".*\")$" + options:options + error:nil]; + if ([attributeRegex numberOfMatchesInString:str options:0 range:NSMakeRange(0, str.length)] != 1) { + return nil; + } + + NSTextCheckingResult *match = [attributeRegex firstMatchInString:str options:0 range:NSMakeRange(0, str.length)]; + str = [str substringWithRange:match.range]; + NSRange range = [str rangeOfString:@"=" options:0]; + + NSString *key = [str substringToIndex:range.location]; + NSString *value = [str substringFromIndex:range.location + 2]; + value = [value substringToIndex:value.length - 1]; + + return @[key, value]; +} + +@end diff --git a/HTMLKitTests/HTMLKitTreeConstructionTests.m b/HTMLKitTests/HTMLKitTreeConstructionTests.m new file mode 100644 index 0000000..33aa5c6 --- /dev/null +++ b/HTMLKitTests/HTMLKitTreeConstructionTests.m @@ -0,0 +1,168 @@ +// +// HTMLKitParserTests.m +// HTMLKit +// +// Created by Iska on 25/03/15. +// Copyright (c) 2015 BrainCookie. All rights reserved. +// + +#import + +#import "HTML5LibTreeConstructionTest.h" +#import "HTMLNodes.h" +#import "HTMLParser.h" + + +#define AssertEqualNodes(a, b, c) \ + do { \ + [self assertNode:a isEqualToNode:b message:c]; \ + } while(0) + +#define AssertEqualChildNodes(a, b, c) \ + do { \ + [self assertElementChildNodes:a areEqualToElementChildNode:b message:c]; \ + } while(0) + +#pragma mark - HTML5Lib Test Suite + +@interface HTMLKitTreeConstructionTests : XCTestCase +@property (nonatomic, strong) NSString *testName; +@property (nonatomic, strong) NSArray *testsList; +@end + +@implementation HTMLKitTreeConstructionTests + ++ (XCTestSuite *)defaultTestSuite +{ + XCTestSuite *suite = [[XCTestSuite alloc] initWithName:NSStringFromClass(self)]; + + NSDictionary *testsMap = [HTML5LibTreeConstructionTest loadHTML5LibTreeConstructionTests]; + [testsMap enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [self addTestCaseForTestFile:key withTests:obj toTestSuite:suite]; + }]; + return suite; +} + ++ (void)addTestCaseForTestFile:(NSString *)testFile withTests:(NSArray *)tests toTestSuite:(XCTestSuite *)suite +{ + NSArray *allInvocations = [self testInvocations]; + for (NSInvocation *invocation in allInvocations) { + XCTestCase *testCase = [[self alloc] initWithInvocation:invocation + testName:testFile + tests:tests]; + [suite addTest:testCase]; + } +} + +#pragma mark - Instance + +- (instancetype)initWithInvocation:(NSInvocation *)invocation + testName:(NSString *)testName + tests:(NSArray *)tests +{ + self = [super initWithInvocation:invocation]; + if (self) { + _testName = testName; + _testsList = tests; + } + return self; +} + +- (NSString *)name +{ + NSInvocation *invocation = [self invocation]; + NSString *title = self.testName.stringByDeletingPathExtension; + return [NSString stringWithFormat:@"-[%@ %@_%@]", self.class, NSStringFromSelector(invocation.selector), title]; +} + +- (NSString *)description +{ + return self.name; +} + +#pragma mark - Tests + +- (void)testParser +{ + for (HTML5LibTreeConstructionTest *test in self.testsList) { + HTMLElement *contextElement = test.documentFragment; + + HTMLParser *parser = [[HTMLParser alloc] initWithString:test.data]; + + NSArray *actual = nil; + if (contextElement == nil) { + actual = [parser parseDocument].childNodes.array; + } else { + actual = [parser parseFragmentWithContextElement:contextElement]; + } + + NSString *expectedNodes = [[test.nodes valueForKey:@"outerHTML"] componentsJoinedByString:@""]; + NSString *message = [NSString stringWithFormat:@"HTML5Lib test in file: \'%@\'\nInput: %@\nExpected:\n%@\nActual:\n%@\n", + test.testFile, + test.data, + expectedNodes, + parser.document.innerHTML]; + + XCTAssertEqual(actual.count, test.nodes.count, @"Nodes mismatch:\n%@", message); + if (actual.count != test.nodes.count) { + continue; + } + + [actual enumerateObjectsUsingBlock:^(HTMLNode *actual, NSUInteger idx, BOOL *stop) { + HTMLNode *expected = [test.nodes objectAtIndex:idx]; + AssertEqualNodes(actual, expected, message); + }]; + } +} + +- (void)assertNode:(HTMLNode *)actual isEqualToNode:(HTMLNode *)expected message:(NSString *)message +{ + XCTAssertEqualObjects(actual.name, expected.name, @"Node name mismatch [%@ should be %@]:\n%@", + actual.name, expected.name, message); + XCTAssert(actual.type == expected.type, @"Node type mismatch [%hd should be %hd]:\n%@", + actual.type, expected.type, message); + + if (actual.type != expected.type) { + return; + } + + switch (actual.type) { + case HTMLNodeDocumentType: + XCTAssertEqualObjects([(HTMLDocumentType *)actual publicIdentifier], [(HTMLDocumentType *)expected publicIdentifier]); + XCTAssertEqualObjects([(HTMLDocumentType *)actual systemIdentifier], [(HTMLDocumentType *)expected systemIdentifier]); + break; + case HTMLNodeElement: + XCTAssertEqualObjects([(HTMLElement *)actual attributes], [(HTMLElement *)expected attributes]); + AssertEqualChildNodes((HTMLElement *)actual, (HTMLElement *)expected, message); + break; + case HTMLNodeComment: + XCTAssertEqualObjects([(HTMLComment *)actual data], [(HTMLComment *)expected data]); + break; + case HTMLNodeText: + XCTAssertEqualObjects([(HTMLText *)actual data], [(HTMLText *)expected data]); + break; + default: + break; + } +} + +- (void)assertElementChildNodes:(HTMLElement *)elemen1 areEqualToElementChildNode:(HTMLElement *)element2 message:(NSString *)message +{ + XCTAssertEqual(elemen1.childNodes.count, element2.childNodes.count, + @"Child nodes count mismatch [element %@ has %lu bu should have %lu child nodes]\n%@", + elemen1, + (unsigned long)elemen1.childNodes.count, + (unsigned long)element2.childNodes.count, + message); + + if (elemen1.childNodes.count != element2.childNodes.count) { + return; + } + + [elemen1.childNodes.array enumerateObjectsUsingBlock:^(HTMLNode *actual, NSUInteger idx, BOOL *stop) { + HTMLNode *expected = [element2.childNodes.array objectAtIndex:idx]; + AssertEqualNodes(actual, expected, message); + }]; +} + +@end