";
+ };
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
|