b693a60358
Do not allocate empty collections for child nodes or attributes when initializing new HTML Nodes or Elements. These are initialized the first time they are accessed. Analogously, the mutable data string of CharacterData is also allocated with the empty string on first access.
763 lines
18 KiB
Objective-C
763 lines
18 KiB
Objective-C
//
|
|
// HTMLNode.m
|
|
// HTMLKit
|
|
//
|
|
// Created by Iska on 24/02/15.
|
|
// Copyright (c) 2015 BrainCookie. All rights reserved.
|
|
//
|
|
|
|
#import "HTMLNode.h"
|
|
#import "HTMLNode+Private.h"
|
|
#import "HTMLDocument.h"
|
|
#import "HTMLDocumentType.h"
|
|
#import "HTMLElement.h"
|
|
#import "HTMLText.h"
|
|
#import "HTMLComment.h"
|
|
#import "HTMLKitDOMExceptions.h"
|
|
#import "HTMLNodeFilter.h"
|
|
#import "CSSSelector.h"
|
|
#import "HTMLDocument+Private.h"
|
|
#import "HTMLDOMUtils.h"
|
|
|
|
@interface HTMLNode ()
|
|
{
|
|
NSMutableOrderedSet *_childNodes;
|
|
}
|
|
@end
|
|
|
|
@implementation HTMLNode
|
|
@synthesize ownerDocument = _ownerDocument;
|
|
|
|
#pragma mark - Init
|
|
|
|
- (instancetype)initWithName:(NSString *)name type:(HTMLNodeType)type
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_name = name;
|
|
_nodeType = type;
|
|
_childNodes = nil;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - Properties
|
|
|
|
- (NSOrderedSet<HTMLNode *> *)childNodes
|
|
{
|
|
if (_childNodes == nil) {
|
|
_childNodes = [NSMutableOrderedSet new];
|
|
}
|
|
|
|
return _childNodes;
|
|
}
|
|
|
|
- (HTMLDocument *)ownerDocument
|
|
{
|
|
if (_nodeType == HTMLNodeDocument) {
|
|
return (HTMLDocument *)self;
|
|
} else {
|
|
return _ownerDocument;
|
|
}
|
|
}
|
|
|
|
- (void)setOwnerDocument:(HTMLDocument *)ownerDocument
|
|
{
|
|
_ownerDocument = ownerDocument;
|
|
[self.childNodes.array makeObjectsPerformSelector:@selector(setOwnerDocument:) withObject:ownerDocument];
|
|
}
|
|
|
|
- (HTMLNode *)rootNode
|
|
{
|
|
return _parentNode == nil ? self : _parentNode.rootNode;
|
|
}
|
|
|
|
- (void)setParentNode:(HTMLNode *)parentNode
|
|
{
|
|
_parentNode = parentNode;
|
|
}
|
|
|
|
- (HTMLElement *)parentElement
|
|
{
|
|
return _parentNode.nodeType == HTMLNodeElement ? (HTMLElement *)_parentNode : nil;
|
|
}
|
|
|
|
- (HTMLNode *)firstChild
|
|
{
|
|
return self.childNodes.firstObject;
|
|
}
|
|
|
|
- (HTMLNode *)lastChild
|
|
{
|
|
return self.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];
|
|
}
|
|
|
|
- (HTMLElement *)previousSiblingElement
|
|
{
|
|
HTMLNode *node = self.previousSibling;
|
|
while (node && node.nodeType != HTMLNodeElement) {
|
|
node = node.previousSibling;
|
|
}
|
|
return node.asElement;
|
|
}
|
|
|
|
- (HTMLElement *)nextSiblingElement
|
|
{
|
|
HTMLNode *node = self.previousSibling;
|
|
while (node && node.nodeType != HTMLNodeElement) {
|
|
node = node.nextSibling;
|
|
}
|
|
return node.asElement;
|
|
}
|
|
|
|
- (NSUInteger)index
|
|
{
|
|
return [self.parentNode indexOfChildNode:self];
|
|
}
|
|
|
|
- (NSString *)textContent
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
- (NSUInteger)length
|
|
{
|
|
return self.childNodesCount;
|
|
}
|
|
|
|
#pragma mark - Cast
|
|
|
|
- (HTMLElement *)asElement
|
|
{
|
|
return (HTMLElement *)self;
|
|
}
|
|
|
|
#pragma mark - Child Nodes
|
|
|
|
- (BOOL)hasChildNodes
|
|
{
|
|
return self.childNodes.count > 0;
|
|
}
|
|
|
|
- (BOOL)hasChildNodeOfType:(HTMLNodeType)type
|
|
{
|
|
NSUInteger index = [self.childNodes indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
|
|
if ([(HTMLNode *)obj nodeType] == type) {
|
|
*stop = YES;
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}];
|
|
|
|
return index != NSNotFound;
|
|
}
|
|
|
|
- (NSUInteger)childNodesCount
|
|
{
|
|
return self.childNodes.count;
|
|
}
|
|
|
|
- (BOOL)isEmpty
|
|
{
|
|
return self.length == 0;
|
|
}
|
|
|
|
- (HTMLNode *)childNodeAtIndex:(NSUInteger)index
|
|
{
|
|
return [self.childNodes objectAtIndex:index];
|
|
}
|
|
|
|
- (NSUInteger)childElementsCount
|
|
{
|
|
return [self.childNodes indexesOfObjectsPassingTest:^BOOL(HTMLNode * _Nonnull node, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
return node.nodeType == HTMLNodeElement;
|
|
}].count;
|
|
}
|
|
|
|
- (NSUInteger)indexOfChildNode:(HTMLNode *)node
|
|
{
|
|
return [self.childNodes indexOfObject:node];
|
|
}
|
|
|
|
- (HTMLElement *)childElementAtIndex:(NSUInteger)index
|
|
{
|
|
NSUInteger counter = 0;
|
|
for (HTMLNode *node in self.childNodes) {
|
|
if (node.nodeType == HTMLNodeElement) {
|
|
if (counter == index) {
|
|
return node.asElement;
|
|
}
|
|
counter++;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSUInteger)indexOfChildElement:(HTMLElement *)element
|
|
{
|
|
NSUInteger counter = 0;
|
|
for (HTMLNode *node in self.childNodes) {
|
|
if (node.nodeType == HTMLNodeElement) {
|
|
if (node == element) {
|
|
return counter;
|
|
}
|
|
counter++;
|
|
}
|
|
}
|
|
return NSNotFound;
|
|
}
|
|
|
|
- (HTMLNode *)prependNode:(HTMLNode *)node
|
|
{
|
|
return [self insertNode:node beforeChildNode:self.firstChild];
|
|
}
|
|
|
|
- (void)prependNodes:(NSArray *)nodes
|
|
{
|
|
for (id node in nodes.reverseObjectEnumerator) {
|
|
[self insertNode:node beforeChildNode:self.firstChild];
|
|
}
|
|
}
|
|
|
|
- (HTMLNode *)appendNode:(HTMLNode *)node
|
|
{
|
|
return [self insertNode:node beforeChildNode:nil];
|
|
}
|
|
|
|
- (void)appendNodes:(NSArray *)nodes
|
|
{
|
|
for (id node in nodes) {
|
|
[self insertNode:node beforeChildNode:nil];
|
|
}
|
|
}
|
|
|
|
- (HTMLNode *)insertNode:(HTMLNode *)node beforeChildNode:(HTMLNode *)child
|
|
{
|
|
#ifndef HTMLKIT_NO_DOM_CHECKS
|
|
[self ensurePreInsertionValidityOfNode:node beforeChildNode:child];
|
|
#endif
|
|
|
|
[self.ownerDocument adoptNode:node];
|
|
|
|
NSArray *nodes = node.nodeType == HTMLNodeDocumentFragment ? [NSArray arrayWithArray:node.childNodes.array] : @[node];
|
|
|
|
NSUInteger index = [self indexOfChildNode:child];
|
|
if (index != NSNotFound) {
|
|
NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(index, nodes.count)];
|
|
[(NSMutableOrderedSet *)self.childNodes insertObjects:nodes atIndexes:indexes];
|
|
} else {
|
|
[(NSMutableOrderedSet *)self.childNodes addObjectsFromArray:nodes];
|
|
}
|
|
|
|
if (node.nodeType == HTMLNodeDocumentFragment) {
|
|
[node removeAllChildNodes];
|
|
}
|
|
|
|
[nodes makeObjectsPerformSelector:@selector(setParentNode:) withObject:self];
|
|
|
|
return node;
|
|
}
|
|
|
|
- (HTMLNode *)replaceChildNode:(HTMLNode *)child withNode:(HTMLNode *)node
|
|
{
|
|
#ifndef HTMLKIT_NO_DOM_CHECKS
|
|
[self ensureReplacementValidityOfChildNode:child withNode:node];
|
|
#endif
|
|
|
|
[self insertNode:node beforeChildNode:child];
|
|
[child removeFromParentNode];
|
|
return child;
|
|
}
|
|
|
|
- (void)replaceAllChildNodesWithNode:(HTMLNode *)node
|
|
{
|
|
[self removeAllChildNodes];
|
|
|
|
if (node != nil) {
|
|
[self.ownerDocument adoptNode:node];
|
|
[self insertNode:node beforeChildNode:nil];
|
|
}
|
|
}
|
|
|
|
- (void)removeFromParentNode
|
|
{
|
|
[self.parentNode removeChildNode:self];
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
|
|
HTMLNode *oldNode = child;
|
|
HTMLNode *oldParent = child.parentNode;
|
|
HTMLNode *oldPreviousSibling = child.previousSibling;
|
|
|
|
[(NSMutableOrderedSet *)self.childNodes removeObject:child];
|
|
child.parentNode = nil;
|
|
|
|
[self.ownerDocument runRemovingStepsForNode:oldNode
|
|
withOldParent:oldParent
|
|
andOldPreviousSibling:oldPreviousSibling];
|
|
return child;
|
|
}
|
|
|
|
- (HTMLNode *)removeChildNodeAtIndex:(NSUInteger)index
|
|
{
|
|
HTMLNode *node = [self childNodeAtIndex:index];
|
|
return [self removeChildNode:node];
|
|
}
|
|
|
|
- (void)reparentChildNodesIntoNode:(HTMLNode *)node
|
|
{
|
|
for (HTMLNode *child in self.childNodes.array) {
|
|
[node appendNode:child];
|
|
}
|
|
[(NSMutableOrderedSet *)self.childNodes removeAllObjects];
|
|
}
|
|
|
|
- (void)removeAllChildNodes
|
|
{
|
|
[self.childNodes.array makeObjectsPerformSelector:@selector(setParentNode:) withObject:nil];
|
|
[(NSMutableOrderedSet *)self.childNodes removeAllObjects];
|
|
}
|
|
|
|
- (HTMLDocumentPosition)compareDocumentPositionWithNode:(HTMLNode *)otherNode
|
|
{
|
|
if (otherNode == nil) {
|
|
return HTMLDocumentPositionDisconnected;
|
|
}
|
|
|
|
if (self == otherNode) {
|
|
return HTMLDocumentPositionEquivalent;
|
|
}
|
|
|
|
if (self.ownerDocument != otherNode.ownerDocument) {
|
|
return (HTMLDocumentPositionDisconnected | HTMLDocumentPositionImplementationSpecific |
|
|
self.hash < otherNode.hash ? HTMLDocumentPositionPreceding : HTMLDocumentPositionFollowing);
|
|
}
|
|
|
|
NSArray *ancestors1 = GetAncestorNodes(self);
|
|
NSArray *ancestors2 = GetAncestorNodes(otherNode);
|
|
|
|
if (ancestors1.lastObject != ancestors2.lastObject) {
|
|
return (HTMLDocumentPositionDisconnected | HTMLDocumentPositionImplementationSpecific |
|
|
self.hash < otherNode.hash ? HTMLDocumentPositionPreceding : HTMLDocumentPositionFollowing);
|
|
}
|
|
|
|
NSUInteger index1 = ancestors1.count;
|
|
NSUInteger index2 = ancestors2.count;
|
|
|
|
for (NSUInteger i = MIN(index1, index2); i; --i) {
|
|
index1 -= 1;
|
|
index2 -= 1;
|
|
|
|
HTMLNode *child1 = ancestors1[index1];
|
|
HTMLNode *child2 = ancestors2[index2];
|
|
|
|
if (child1 != child2) {
|
|
for (HTMLNode *sibling = child1.nextSibling; sibling; sibling = sibling.nextSibling) {
|
|
if (sibling == child2) {
|
|
return HTMLDocumentPositionPreceding;
|
|
}
|
|
}
|
|
return HTMLDocumentPositionFollowing;
|
|
}
|
|
}
|
|
|
|
if (ancestors1.count < ancestors2.count) {
|
|
return HTMLDocumentPositionContains | HTMLDocumentPositionPreceding;
|
|
} else {
|
|
return HTMLDocumentPositionContainedBy | HTMLDocumentPositionFollowing;
|
|
}
|
|
}
|
|
|
|
- (BOOL)isDescendantOfNode:(HTMLNode *)otherNode
|
|
{
|
|
if (otherNode == nil) {
|
|
return NO;
|
|
}
|
|
|
|
if (self.ownerDocument != otherNode.ownerDocument) {
|
|
return NO;
|
|
}
|
|
|
|
if (!otherNode.hasChildNodes) {
|
|
return NO;
|
|
}
|
|
|
|
if (otherNode.nodeType == HTMLNodeDocument) {
|
|
return self.nodeType != HTMLNodeDocument && self.ownerDocument == otherNode;
|
|
}
|
|
|
|
for (HTMLNode *parentNode = self.parentNode; parentNode; parentNode = parentNode.parentNode) {
|
|
if (parentNode == otherNode) {
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)containsNode:(HTMLNode *)otherNode
|
|
{
|
|
return self == otherNode || [otherNode isDescendantOfNode:self];
|
|
}
|
|
|
|
#pragma mark - Enumeration
|
|
|
|
- (void)enumerateChildNodesUsingBlock:(void (^)(HTMLNode *node, NSUInteger idx, BOOL *stop))block
|
|
{
|
|
if (block == nil) {
|
|
return;
|
|
}
|
|
|
|
[self.childNodes enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
|
block(obj, idx, stop);
|
|
}];
|
|
}
|
|
|
|
- (void)enumerateChildElementsUsingBlock:(void (^)(HTMLElement *element, NSUInteger idx, BOOL *stop))block
|
|
{
|
|
if (block == nil) {
|
|
return;
|
|
}
|
|
|
|
[self.childNodes enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
|
if ([obj isKindOfClass:[HTMLElement class]]) {
|
|
block([obj asElement], idx, stop);
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (HTMLNodeIterator *)nodeIterator
|
|
{
|
|
return [self nodeIteratorWithShowOptions:HTMLNodeFilterShowAll filter:nil];
|
|
}
|
|
|
|
- (HTMLNodeIterator *)nodeIteratorWithShowOptions:(HTMLNodeFilterShowOptions)showOptions
|
|
filter:(id<HTMLNodeFilter>)filter
|
|
{
|
|
return [[HTMLNodeIterator alloc] initWithNode:self showOptions:showOptions filter:filter];
|
|
}
|
|
|
|
- (HTMLNodeIterator *)nodeIteratorWithShowOptions:(HTMLNodeFilterShowOptions)showOptions
|
|
filterBlock:(HTMLNodeFilterValue (^)(HTMLNode *node))block
|
|
{
|
|
HTMLNodeFilterBlock *filter = [HTMLNodeFilterBlock filterWithBlock:block];
|
|
return [[HTMLNodeIterator alloc] initWithNode:self showOptions:showOptions filter:filter];
|
|
}
|
|
|
|
#pragma mark - Selectors
|
|
|
|
- (HTMLElement *)querySelector:(NSString *)selectorString
|
|
{
|
|
CSSSelector *selector = [CSSSelector selectorWithString:selectorString];
|
|
return [self firstElementMatchingSelector:selector];
|
|
}
|
|
|
|
- (NSArray<HTMLElement *> *)querySelectorAll:(NSString *)selectorString
|
|
{
|
|
CSSSelector *selector = [CSSSelector selectorWithString:selectorString];
|
|
return [self elementsMatchingSelector:selector];
|
|
}
|
|
|
|
- (HTMLElement *)firstElementMatchingSelector:(CSSSelector *)selector
|
|
{
|
|
if (selector == nil) {
|
|
return nil;
|
|
}
|
|
|
|
for (HTMLElement *element in [self nodeIteratorWithShowOptions:HTMLNodeFilterShowElement filter:nil]) {
|
|
if ([selector acceptElement:element]) {
|
|
return element;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSArray<HTMLElement *> *)elementsMatchingSelector:(CSSSelector *)selector
|
|
{
|
|
if (selector == nil) {
|
|
return @[];
|
|
}
|
|
|
|
NSMutableArray *result = [NSMutableArray array];
|
|
for (HTMLElement *element in [self nodeIteratorWithShowOptions:HTMLNodeFilterShowElement filter:nil]) {
|
|
if ([selector acceptElement:element]) {
|
|
[result addObject:element];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#ifndef HTMLKIT_NO_DOM_CHECKS
|
|
|
|
#pragma mark - Validity Checks
|
|
|
|
NS_INLINE void CheckParentValid(HTMLNode *parent, NSString *cmd)
|
|
{
|
|
if (parent.nodeType != HTMLNodeDocument &&
|
|
parent.nodeType != HTMLNodeDocumentFragment &&
|
|
parent.nodeType != HTMLNodeElement) {
|
|
[NSException raise:HTMLKitHierarchyRequestError
|
|
format:@"%@: Hierarchy Request Error, inserting into %@ is not allowed. The operation would yield an incorrect node tree.",
|
|
cmd, parent.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.nodeType != HTMLNodeDocumentFragment &&
|
|
node.nodeType != HTMLNodeDocumentType &&
|
|
node.nodeType != HTMLNodeElement &&
|
|
node.nodeType != HTMLNodeText &&
|
|
node.nodeType != 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.nodeType == HTMLNodeText && parent.nodeType == 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.nodeType == HTMLNodeDocumentType && parent.nodeType != 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, inserting (%@) into (%@). The operation would yield an incorrect node tree.",
|
|
NSStringFromSelector(_cmd), self, node];
|
|
};
|
|
|
|
if (self.nodeType == HTMLNodeDocument) {
|
|
switch (node.nodeType) {
|
|
case HTMLNodeDocumentFragment:
|
|
if (node.childNodesCount > 1 ||
|
|
[node hasChildNodeOfType:HTMLNodeText]) {
|
|
hierarchyError();
|
|
} else if (node.childNodesCount == 1) {
|
|
if ([self hasChildNodeOfType:HTMLNodeElement] ||
|
|
child.nodeType == HTMLNodeDocumentType ||
|
|
child.nextSibling.nodeType == HTMLNodeDocumentType) {
|
|
hierarchyError();
|
|
}
|
|
}
|
|
break;
|
|
case HTMLNodeElement:
|
|
if ([self hasChildNodeOfType:HTMLNodeElement] ||
|
|
child.nodeType == HTMLNodeDocumentType ||
|
|
(child != nil && child.nextSibling.nodeType == HTMLNodeDocumentType)) {
|
|
hierarchyError();
|
|
}
|
|
break;
|
|
case HTMLNodeDocumentType:
|
|
if ([self hasChildNodeOfType:HTMLNodeDocumentType] ||
|
|
child.previousSibling != nil ||
|
|
(child == nil && [self hasChildNodeOfType:HTMLNodeElement])) {
|
|
hierarchyError();
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)ensureReplacementValidityOfChildNode:(HTMLNode *)child withNode:(HTMLNode *)node
|
|
{
|
|
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)];
|
|
};
|
|
|
|
void (^ checkParentHasAnotherChildOfType)(HTMLNodeType) = ^ void (HTMLNodeType type) {
|
|
[self enumerateChildNodesUsingBlock:^(HTMLNode *node, NSUInteger idx, BOOL *stop) {
|
|
if (node.nodeType == type && node != child) {
|
|
*stop = YES;
|
|
hierarchyError();
|
|
}
|
|
}];
|
|
};
|
|
|
|
if (self.nodeType == HTMLNodeDocument) {
|
|
switch (node.nodeType) {
|
|
case HTMLNodeDocumentFragment:
|
|
if (node.childNodesCount > 1 ||
|
|
[node hasChildNodeOfType:HTMLNodeText]) {
|
|
hierarchyError();
|
|
} else if (node.childNodesCount == 1) {
|
|
if (child.nextSibling.nodeType == HTMLNodeDocumentType) {
|
|
hierarchyError();
|
|
}
|
|
checkParentHasAnotherChildOfType(HTMLNodeElement);
|
|
}
|
|
break;
|
|
case HTMLNodeElement:
|
|
{
|
|
if (child.nextSibling.nodeType == HTMLNodeDocumentType) {
|
|
hierarchyError();
|
|
}
|
|
checkParentHasAnotherChildOfType(HTMLNodeElement);
|
|
break;
|
|
}
|
|
case HTMLNodeDocumentType:
|
|
{
|
|
if (child.previousSibling.nodeType == HTMLNodeElement) {
|
|
hierarchyError();
|
|
}
|
|
checkParentHasAnotherChildOfType(HTMLNodeDocumentType);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
#pragma mark - Clone
|
|
|
|
- (instancetype)cloneNodeDeep:(BOOL)deep
|
|
{
|
|
HTMLNode *copy = [self copy];
|
|
|
|
if (deep) {
|
|
for (HTMLNode *child in self.childNodes) {
|
|
[copy appendNode:[child cloneNodeDeep:YES]];
|
|
}
|
|
}
|
|
|
|
return copy;
|
|
}
|
|
|
|
#pragma mark - NSCopying
|
|
|
|
- (id)copyWithZone:(NSZone *)zone
|
|
{
|
|
HTMLNode *copy = [[self.class alloc] initWithName:self.name type:self.nodeType];
|
|
return copy;
|
|
}
|
|
|
|
#pragma mark - Serialization
|
|
|
|
- (NSString *)outerHTML
|
|
{
|
|
[self doesNotRecognizeSelector:_cmd];
|
|
return nil;
|
|
}
|
|
|
|
- (NSString *)innerHTML
|
|
{
|
|
return [[self.childNodes.array valueForKey:@"outerHTML"] componentsJoinedByString:@""];
|
|
}
|
|
|
|
- (void)setInnerHTML:(NSString *)outerHTML
|
|
{
|
|
[self doesNotRecognizeSelector:_cmd];
|
|
}
|
|
|
|
#pragma mark - Description
|
|
|
|
- (NSString *)treeDescription
|
|
{
|
|
NSMutableString *string = [NSMutableString string];
|
|
|
|
__weak __block void (^ weakAccumulator) (HTMLNode *, NSUInteger);
|
|
void (^ accumulator) (HTMLNode *, NSUInteger);
|
|
static NSString *prefix = @"| ";
|
|
|
|
weakAccumulator = accumulator = ^ (HTMLNode *node, NSUInteger level) {
|
|
|
|
NSString *indent = [prefix stringByPaddingToLength:level * 2 + prefix.length
|
|
withString:@" "
|
|
startingAtIndex:0];
|
|
if (level > 0) {
|
|
[string appendString:@"\n"];
|
|
}
|
|
|
|
[string appendString:indent];
|
|
[string appendString:node.description];
|
|
|
|
for (HTMLNode *child in node.childNodes) {
|
|
weakAccumulator(child, level + 1);
|
|
}
|
|
};
|
|
accumulator(self, 0);
|
|
|
|
return string;
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"<%@: %p '%@'>", self.class, self, self.name];
|
|
}
|
|
|
|
- (id)debugQuickLookObject
|
|
{
|
|
return self.outerHTML;
|
|
}
|
|
|
|
@end
|