From d8d191cb5aabfd2876c57fbfc0c3952a8071c4c1 Mon Sep 17 00:00:00 2001 From: iska Date: Thu, 19 Mar 2015 00:42:43 +0100 Subject: [PATCH] Add implementation for the HTML Node and its Mutation Algorithms https://dom.spec.whatwg.org/#mutation-algorithms --- HTMLKit.xcodeproj/project.pbxproj | 10 + HTMLKit/HTMLDocument.h | 2 + HTMLKit/HTMLKitExceptions.h | 11 + HTMLKit/HTMLKitExceptions.m | 13 ++ HTMLKit/HTMLNode.h | 16 +- HTMLKit/HTMLNode.m | 324 ++++++++++++++++++++++++++++++ 6 files changed, 372 insertions(+), 4 deletions(-) create mode 100644 HTMLKit/HTMLKitExceptions.h create mode 100644 HTMLKit/HTMLKitExceptions.m diff --git a/HTMLKit.xcodeproj/project.pbxproj b/HTMLKit.xcodeproj/project.pbxproj index 1abdac0..502c0c7 100644 --- a/HTMLKit.xcodeproj/project.pbxproj +++ b/HTMLKit.xcodeproj/project.pbxproj @@ -37,6 +37,9 @@ 6238579B1A9E8934003A45D9 /* HTMLComment.h in Headers */ = {isa = PBXBuildFile; fileRef = 623857991A9E8934003A45D9 /* HTMLComment.h */; }; 6238579C1A9E8934003A45D9 /* HTMLComment.m in Sources */ = {isa = PBXBuildFile; fileRef = 6238579A1A9E8934003A45D9 /* HTMLComment.m */; }; 6238579D1A9E8934003A45D9 /* HTMLComment.m in Sources */ = {isa = PBXBuildFile; fileRef = 6238579A1A9E8934003A45D9 /* HTMLComment.m */; }; + 6238C9851AB8D6330006512E /* HTMLKitExceptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 6238C9831AB8D6330006512E /* HTMLKitExceptions.h */; }; + 6238C9861AB8D6330006512E /* HTMLKitExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 6238C9841AB8D6330006512E /* HTMLKitExceptions.m */; }; + 6238C9871AB8D6330006512E /* HTMLKitExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 6238C9841AB8D6330006512E /* HTMLKitExceptions.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 */; }; @@ -152,6 +155,8 @@ 623857951A9E8606003A45D9 /* HTMLDocumentType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLDocumentType.m; sourceTree = ""; }; 623857991A9E8934003A45D9 /* HTMLComment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLComment.h; sourceTree = ""; }; 6238579A1A9E8934003A45D9 /* HTMLComment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLComment.m; sourceTree = ""; }; + 6238C9831AB8D6330006512E /* HTMLKitExceptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLKitExceptions.h; sourceTree = ""; }; + 6238C9841AB8D6330006512E /* HTMLKitExceptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLKitExceptions.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 = ""; }; @@ -328,6 +333,8 @@ 623424871AB467B200726190 /* HTMLOrderedDictionary.m */, 6235CE991AA509430026937B /* NSString+HTMLKit.h */, 6235CE9A1AA509430026937B /* NSString+HTMLKit.m */, + 6238C9831AB8D6330006512E /* HTMLKitExceptions.h */, + 6238C9841AB8D6330006512E /* HTMLKitExceptions.m */, 623719441AA1472B002E03C8 /* Nodes */, 625A14B619C7829400AD0C32 /* Supporting Files */, ); @@ -415,6 +422,7 @@ 6235CEA01AA5170A0026937B /* HTMLMarker.h in Headers */, 62F31FDD19E9DCCF007F0657 /* HTMLTokenizerEntities.h in Headers */, 6234BEEE1AABBF1400DEB15F /* HTMLStackOfOpenElements.h in Headers */, + 6238C9851AB8D6330006512E /* HTMLKitExceptions.h in Headers */, 6238039F1AB63A8C008A53D0 /* HTMLEOFToken.h in Headers */, 6279F87419E1808D00F12EE5 /* HTMLElement.h in Headers */, 62AE593F19F9907C0043F069 /* HTMLTagToken.h in Headers */, @@ -589,6 +597,7 @@ 62AE594119F9907C0043F069 /* HTMLTagToken.m in Sources */, 62AE594619F992F30043F069 /* HTMLCommentToken.m in Sources */, 62AE593C19F97E1C0043F069 /* HTMLDOCTYPEToken.m in Sources */, + 6238C9871AB8D6330006512E /* HTMLKitExceptions.m in Sources */, 62F873ED19E088C90062683C /* HTMLParser.m in Sources */, 623803A11AB63A8C008A53D0 /* HTMLEOFToken.m in Sources */, 6279F87619E1808D00F12EE5 /* HTMLElement.m in Sources */, @@ -628,6 +637,7 @@ 62AE594019F9907C0043F069 /* HTMLTagToken.m in Sources */, 62AE594519F992F30043F069 /* HTMLCommentToken.m in Sources */, 62AE593B19F97E1C0043F069 /* HTMLDOCTYPEToken.m in Sources */, + 6238C9861AB8D6330006512E /* HTMLKitExceptions.m in Sources */, 62F873EC19E088C90062683C /* HTMLParser.m in Sources */, 623803A01AB63A8C008A53D0 /* HTMLEOFToken.m in Sources */, 6279F87519E1808D00F12EE5 /* HTMLElement.m in Sources */, diff --git a/HTMLKit/HTMLDocument.h b/HTMLKit/HTMLDocument.h index 213cb67..1fb38fd 100644 --- a/HTMLKit/HTMLDocument.h +++ b/HTMLKit/HTMLDocument.h @@ -27,4 +27,6 @@ typedef NS_ENUM(short, HTMLDocumentReadyState) @property (nonatomic, assign, readonly) HTMLDocumentReadyState readyState; +- (HTMLNode *)adoptNode:(HTMLNode *)node; + @end diff --git a/HTMLKit/HTMLKitExceptions.h b/HTMLKit/HTMLKitExceptions.h new file mode 100644 index 0000000..4520eb0 --- /dev/null +++ b/HTMLKit/HTMLKitExceptions.h @@ -0,0 +1,11 @@ +// +// HTMLKitExceptions.h +// HTMLKit +// +// Created by Iska on 17/03/15. +// Copyright (c) 2015 BrainCookie. All rights reserved. +// + +FOUNDATION_EXPORT NSString * const HTMLKitHierarchyRequestError; +FOUNDATION_EXPORT NSString * const HTMLKitNotFoundError; +FOUNDATION_EXPORT NSString * const HTMLKitNotSupportedError; diff --git a/HTMLKit/HTMLKitExceptions.m b/HTMLKit/HTMLKitExceptions.m new file mode 100644 index 0000000..8234e00 --- /dev/null +++ b/HTMLKit/HTMLKitExceptions.m @@ -0,0 +1,13 @@ +// +// HTMLKitExceptions.m +// HTMLKit +// +// Created by Iska on 17/03/15. +// Copyright (c) 2015 BrainCookie. All rights reserved. +// + +#import "HTMLKitExceptions.h" + +NSString * const HTMLKitHierarchyRequestError = @"HierarchyRequestError"; +NSString * const HTMLKitNotFoundError = @"NotFoundError"; +NSString * const HTMLKitNotSupportedError = @"NotSupportedError"; diff --git a/HTMLKit/HTMLNode.h b/HTMLKit/HTMLNode.h index 57cc975..2347e43 100644 --- a/HTMLKit/HTMLNode.h +++ b/HTMLKit/HTMLNode.h @@ -33,7 +33,7 @@ typedef NS_ENUM(short, HTMLNodeType) @property (nonatomic, strong, readonly) NSString *name; -@property (nonatomic, strong, readonly) HTMLDocument *document; +@property (nonatomic, strong, readonly) HTMLDocument *ownerDocument; @property (nonatomic, strong, readonly) NSString *baseURI; @@ -57,16 +57,24 @@ typedef NS_ENUM(short, HTMLNodeType) - (BOOL)hasChildNodes; +- (BOOL)hasChildNodeOfType:(HTMLNodeType)type; + +- (void)enumerateChildNodesUsingBlock:(void (^)(HTMLNode *node, NSUInteger idx, BOOL *stop))block; + +- (NSUInteger)childNodesCount; + - (HTMLNode *)childNodeAtIndex:(NSUInteger)index; - (NSUInteger)indexOfChildNode:(HTMLNode *)node; -- (HTMLNode *)insertNodeBefore:(HTMLNode *)node; +- (HTMLNode *)insertNode:(HTMLNode *)node beforeChildNode:(HTMLNode *)child; -- (HTMLNode *)appendChildNode:(HTMLNode *)node; +- (HTMLNode *)appendNode:(HTMLNode *)node; -- (HTMLNode *)replaceChildNode:(HTMLNode *)node; +- (HTMLNode *)replaceChildNode:(HTMLNode *)node withNode:(HTMLNode *)replacement; - (HTMLNode *)removeChildNode:(HTMLNode *)node; +- (HTMLNode *)removeChildNodeAtIndex:(NSUInteger)index; + @end diff --git a/HTMLKit/HTMLNode.m b/HTMLKit/HTMLNode.m index 5e4acdc..7c78fd1 100644 --- a/HTMLKit/HTMLNode.m +++ b/HTMLKit/HTMLNode.m @@ -7,7 +7,331 @@ // #import "HTMLNode.h" +#import "HTMLDocument.h" +#import "HTMLElement.h" +#import "HTMLKitExceptions.h" + +@interface HTMLNode () +{ + NSMutableOrderedSet *_childNodes; +} +@end @implementation HTMLNode +#pragma mark - Init + +- (instancetype)initWithName:(NSString *)name type:(HTMLNodeType)type +{ + self = [super init]; + if (self) { + _name = name; + _type = type; + _childNodes = [NSMutableOrderedSet new]; + } + return self; +} + +#pragma mark - Properties + +- (HTMLDocument *)ownerDocument +{ + if (_type == HTMLNodeDocument) { + return nil; + } else { + return _parentNode.ownerDocument; + } +} + +- (void)setBaseURI:(NSString *)baseURI +{ + _baseURI = [baseURI copy]; + [_childNodes.array makeObjectsPerformSelector:@selector(setBaseURI:) withObject:baseURI]; +} + +- (void)setParentNode:(HTMLNode *)parentNode +{ + _parentNode = parentNode; + [_childNodes.array makeObjectsPerformSelector:@selector(setParentNode:) withObject:self]; +} + +- (HTMLElement *)parentElement +{ + return _parentNode.type == HTMLNodeElement ? (HTMLElement *)_parentNode : nil; +} + +- (HTMLNode *)firstChiledNode +{ + return _childNodes.firstObject; +} + +- (HTMLNode *)lastChildNode +{ + return _childNodes.lastObject; +} + +- (HTMLNode *)previousSibling +{ + NSUInteger index = [_parentNode indexOfChildNode:self]; + if (index <= 0) { + return nil; + } + return [_parentNode childNodeAtIndex:index - 1]; +} + +- (HTMLNode *)nextSibling +{ + NSUInteger index = [_parentNode indexOfChildNode:self]; + if (index >= _parentNode.childNodesCount - 1) { + return nil; + } + return [_parentNode childNodeAtIndex:index + 1]; +} + +- (NSString *)textContent +{ + return nil; +} + +#pragma mark - Child Nodes + +- (BOOL)hasChildNodes +{ + return _childNodes.count > 0; +} + +- (BOOL)hasChildNodeOfType:(HTMLNodeType)type +{ + NSUInteger index = [_childNodes indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { + if ([(HTMLNode *)obj type] == type) { + *stop = YES; + return YES; + } + return NO; + }]; + + return index != NSNotFound; +} + +- (void)enumerateChildNodesUsingBlock:(void (^)(HTMLNode *node, NSUInteger idx, BOOL *stop))block +{ + if (block == nil) { + return; + } + + [_childNodes enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + block(obj, idx, stop); + }]; +} + +- (NSUInteger)childNodesCount +{ + return _childNodes.count; +} + +- (HTMLNode *)childNodeAtIndex:(NSUInteger)index +{ + return [_childNodes objectAtIndex:index]; +} + +- (NSUInteger)indexOfChildNode:(HTMLNode *)node +{ + return [_childNodes indexOfObject:node]; +} + +- (HTMLNode *)insertNode:(HTMLNode *)node beforeChildNode:(HTMLNode *)child +{ + return [_parentNode preInsertNode:node beforeChildNode:child]; +} + +- (HTMLNode *)appendNode:(HTMLNode *)node +{ + return [self preInsertNode:node beforeChildNode:nil]; +} + +- (HTMLNode *)replaceChildNode:(HTMLNode *)child withNode:(HTMLNode *)node +{ + [self ensureReplacementValidityOfChildNode:child withNode:node]; + + [self.ownerDocument adoptNode:node]; + NSUInteger index = [self indexOfChildNode:child]; + [_childNodes replaceObjectAtIndex:index withObject:node]; + return child; +} + +- (HTMLNode *)removeChildNode:(HTMLNode *)child +{ + if (child.parentNode != self) { + [NSException raise:HTMLKitNotFoundError + format:@"%@: Not Fount Error, removing non-child node %@. The object can not be found here.", + NSStringFromSelector(_cmd), child]; + } + + [_childNodes removeObject:child]; + child.parentNode = nil; + return child; +} + +- (HTMLNode *)removeChildNodeAtIndex:(NSUInteger)index +{ + HTMLNode *node = [self childNodeAtIndex:index]; + return [self removeChildNode:node]; +} + +#pragma mark - Mutation Algorithms + +- (HTMLNode *)preInsertNode:(HTMLNode *)node beforeChildNode:(HTMLNode *)child +{ + [self ensurePreInsertionValidityOfNode:node beforeChildNode:child]; + [self.ownerDocument adoptNode:node]; + NSUInteger index = [self indexOfChildNode:child]; + [_childNodes insertObject:node atIndex:index]; + + return node; +} + +NS_INLINE void CheckParentValid(HTMLNode *node, NSString *cmd) +{ + if (node.type != HTMLNodeDocument && + node.type != HTMLNodeElement) { + [NSException raise:HTMLKitHierarchyRequestError + format:@"%@: Hierarchy Request Error, inserting into %@ is not allowed. The operation would yield an incorrect node tree.", + cmd, node.name]; + } +} + +NS_INLINE void CheckChildsParent(HTMLNode *parent, HTMLNode *child, NSString *cmd) +{ + if (child != nil && + child.parentNode != parent) { + [NSException raise:HTMLKitNotFoundError + format:@"%@: Not Fount Error, insering before non-child node %@. The object can not be found here.", + cmd, child]; + } +} + +NS_INLINE void CheckInsertedNodeValid(HTMLNode *node, NSString *cmd) +{ + if (node.type != HTMLNodeDocumentType && + node.type != HTMLNodeElement && + node.type != HTMLNodeElement && + node.type != HTMLNodeText && + node.type != HTMLNodeComment) { + [NSException raise:HTMLKitHierarchyRequestError + format:@"%@: Hierarchy Request Error, inserting a %@ is not allowed. The operation would yield an incorrect node tree.", + cmd, node.name]; + } +} + +NS_INLINE void CheckInvalidCombination(HTMLNode *parent, HTMLNode *node, NSString *cmd) +{ + if (node.type == HTMLNodeText && parent.type == HTMLNodeDocument) { + [NSException raise:HTMLKitHierarchyRequestError + format:@"%@: Hierarchy Request Error, inserting a text node %@ into docuement is not allowed. The operation would yield an incorrect node tree.", + cmd, parent.name]; + } + + if (node.type == HTMLNodeDocumentType && parent.type != HTMLNodeDocument) { + [NSException raise:HTMLKitHierarchyRequestError + format:@"%@: Hierarchy Request Error, inserting a doctype %@ into a non-document node is not allowed. The operation would yield an incorrect node tree.", + cmd, parent.name]; + } +} + +- (void)ensurePreInsertionValidityOfNode:(HTMLNode *)node beforeChildNode:(HTMLNode *)child +{ + CheckParentValid(self, NSStringFromSelector(_cmd)); + + CheckChildsParent(self, child, NSStringFromSelector(_cmd)); + + CheckInsertedNodeValid(node, NSStringFromSelector(_cmd)); + + CheckInvalidCombination(self, node, NSStringFromSelector(_cmd)); + + void (^ hierarchyError)() = ^{ + [NSException raise:HTMLKitHierarchyRequestError + format:@"%@: Hierarchy Request Error. The operation would yield an incorrect node tree.", + NSStringFromSelector(_cmd)]; + }; + + if (self.type == HTMLNodeDocument) { + switch (node.type) { + case HTMLNodeElement: + if ([self hasChildNodeOfType:HTMLNodeElement] || + child.type == HTMLNodeDocumentType || + (child != nil && child.nextSibling.type == HTMLNodeDocumentType)) { + hierarchyError(); + } + break; + case HTMLNodeDocumentType: + if ([self hasChildNodeOfType:HTMLNodeDocumentType] || + child.previousSibling != nil || + (child == nil && [self hasChildNodeOfType:HTMLNodeElement])) { + hierarchyError(); + } + break; + default: + break; + } + } +} + +- (void)ensureReplacementValidityOfChildNode:(HTMLNode *)node withNode:(HTMLNode *)child +{ + CheckParentValid(self, NSStringFromSelector(_cmd)); + + CheckChildsParent(self, child, NSStringFromSelector(_cmd)); + + CheckInsertedNodeValid(node, NSStringFromSelector(_cmd)); + + CheckInvalidCombination(self, node, NSStringFromSelector(_cmd)); + + void (^ hierarchyError)() = ^{ + [NSException raise:HTMLKitHierarchyRequestError + format:@"%@: Hierarchy Request Error. The operation would yield an incorrect node tree.", + NSStringFromSelector(_cmd)]; + }; + + if (self.type == HTMLNodeDocument) { + switch (node.type) { + case HTMLNodeElement: + { + if (child.nextSibling.type == HTMLNodeDocumentType) { + hierarchyError(); + } + [self enumerateChildNodesUsingBlock:^(HTMLNode *node, NSUInteger idx, BOOL *stop) { + if (node.type == HTMLNodeElement && node != child) { + *stop = YES; + hierarchyError(); + } + }]; + break; + } + case HTMLNodeDocumentType: + { + if (child.previousSibling.type == HTMLNodeElement) { + hierarchyError(); + } + [self enumerateChildNodesUsingBlock:^(HTMLNode *node, NSUInteger idx, BOOL *stop) { + if (node.type == HTMLNodeDocument && node != child) { + *stop = YES; + hierarchyError(); + } + }]; + break; + } + default: + break; + } + } +} + +#pragma mark - NSCopying + +- (id)copyWithZone:(NSZone *)zone +{ + HTMLNode *copy = [[HTMLNode alloc] initWithName:self.name type:self.type]; + return copy; +} + @end