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