//
// HTMLRange.m
// HTMLKit
//
// Created by Iska on 20/11/16.
// Copyright © 2016 BrainCookie. All rights reserved.
//
#import "HTMLRange.h"
#import "HTMLDocument.h"
#import "HTMLKitDOMExceptions.h"
#import "HTMLDocument+Private.h"
#import "HTMLDOMUtils.h"
#import "HTMLNodeTraversal.h"
@interface HTMLRange ()
{
HTMLDocument *_ownerDocument;
}
@end
@implementation HTMLRange
#pragma mark - Lifecycle
- (instancetype)initWithDowcument:(HTMLDocument *)document
{
return [self initWithDowcument:document startContainer:document startOffset:0 endContainer:document endOffset:0];
}
- (instancetype)initWithDowcument:(HTMLDocument *)document
startContainer:(HTMLNode *)startContainer startOffset:(NSUInteger)startOffset
endContainer:(HTMLNode *)endContainer endOffset:(NSUInteger)endOffset
{
self = [super init];
if (self) {
_ownerDocument = document;
[_ownerDocument attachRange:self];
[self setStartNode:startContainer startOffset:startOffset];
[self setEndNode:endContainer endOffset:endOffset];
}
return self;
}
- (void)dealloc
{
[_ownerDocument detachRange:self];
}
#pragma mark - Properties
- (BOOL)isCollapsed
{
return _startContainer == _endContainer && _startOffset == _endOffset;
}
- (HTMLNode *)commonAncestorContainer
{
return GetCommonAncestorContainer(_startContainer, _endContainer);
}
- (HTMLNode *)rootNode
{
return _startContainer.rootNode;
}
#pragma mark - Boundaries
NS_INLINE void CheckValidBoundaryNode(HTMLDocument *document, HTMLNode *node, NSString *cmd)
{
if (node.ownerDocument != document) {
[NSException raise:HTMLKitWrongDocumentError
format:@"%@: Invalid Node Error, %@ is not in the same document.",
cmd, node];
}
}
NS_INLINE void CheckValidBoundaryNodeType(HTMLNode *node, NSString *cmd)
{
if (node == nil || node.nodeType == HTMLNodeDocumentType) {
[NSException raise:HTMLKitInvalidNodeTypeError
format:@"%@: Invalid Node Type Error, %@ is not a valid range boundary node.",
cmd, node];
}
}
NS_INLINE void CheckValidBoundaryOffset(HTMLNode *node, NSUInteger offset, NSString *cmd)
{
if (node.length < offset) {
[NSException raise:HTMLKitIndexSizeError
format:@"%@: Index Size Error, invalid index %lu for range boundary node %@.",
cmd, (unsigned long)offset, node];
}
}
NS_INLINE void CheckValidDocument(HTMLRange *lhs, HTMLRange *rhs, NSString *cmd)
{
if (lhs.rootNode != rhs.rootNode) {
[NSException raise:HTMLKitWrongDocumentError
format:@"%@: Wrong Document Error, ranges %@ and %@ are not in the same document.",
cmd, lhs, rhs];
}
}
NS_INLINE NSComparisonResult CompareBoundaries(HTMLNode *startNode, NSUInteger startOffset, HTMLNode *endNode, NSUInteger endOffset)
{
if (startNode == endNode) {
if (startOffset == endOffset) {
return NSOrderedSame;
} else if (startOffset < endOffset) {
return NSOrderedAscending;
} else {
return NSOrderedDescending;
}
}
HTMLDocumentPosition position = [startNode compareDocumentPositionWithNode:endNode];
if ((position & HTMLDocumentPositionFollowing) == HTMLDocumentPositionFollowing) {
if (CompareBoundaries(endNode, endOffset, startNode, startOffset) == NSOrderedAscending) {
return NSOrderedDescending;
} else {
return NSOrderedAscending;
}
}
if ((position & HTMLDocumentPositionContains) == HTMLDocumentPositionContains) {
HTMLNode *child = endNode;
while (child.parentNode != startNode) {
child = child.parentNode;
}
if (child.index < startOffset) {
return NSOrderedDescending;
}
}
return NSOrderedAscending;
}
- (void)setStartNode:(HTMLNode *)node startOffset:(NSUInteger)offset
{
CheckValidBoundaryNode(_ownerDocument, node, NSStringFromSelector(_cmd));
CheckValidBoundaryNodeType(node, NSStringFromSelector(_cmd));
CheckValidBoundaryOffset(node, offset, NSStringFromSelector(_cmd));
if (self.rootNode != node.rootNode ||
CompareBoundaries(node, offset, _endContainer, _endOffset) == NSOrderedDescending) {
_endContainer = node;
_endOffset = offset;
}
_startContainer = node;
_startOffset = offset;
}
- (void)setEndNode:(HTMLNode *)node endOffset:(NSUInteger)offset
{
CheckValidBoundaryNode(_ownerDocument, node, NSStringFromSelector(_cmd));
CheckValidBoundaryNodeType(node, NSStringFromSelector(_cmd));
CheckValidBoundaryOffset(node, offset, NSStringFromSelector(_cmd));
if (self.rootNode != node.rootNode ||
CompareBoundaries(node, offset, _startContainer, _startOffset) == NSOrderedAscending) {
_startContainer = node;
_startOffset = offset;
}
_endContainer = node;
_endOffset = offset;
}
- (void)setStartBeforeNode:(HTMLNode *)node
{
HTMLNode *parent = node.parentNode;
[self setStartNode:parent startOffset:node.index];
}
- (void)setStartAfterNode:(HTMLNode *)node
{
HTMLNode *parent = node.parentNode;
[self setStartNode:parent startOffset:node.index + 1];
}
- (void)setEndBeforeNode:(HTMLNode *)node
{
HTMLNode *parent = node.parentNode;
[self setEndNode:parent endOffset:node.index];
}
- (void)setEndAfterNode:(HTMLNode *)node
{
HTMLNode *parent = node.parentNode;
[self setEndNode:parent endOffset:node.index + 1];
}
- (void)collapseToStart
{
[self setEndNode:_startContainer endOffset:_startOffset];
}
- (void)collapseToEnd
{
[self setStartNode:_endContainer startOffset:_endOffset];
}
- (void)selectNode:(HTMLNode *)node
{
HTMLNode *parent = node.parentNode;
[self setStartNode:parent startOffset:node.index];
[self setEndNode:parent endOffset:node.index + 1];
}
- (void)selectNodeContents:(HTMLNode *)node
{
[self setStartNode:node startOffset:0];
[self setEndNode:node endOffset:node.length];
}
- (NSComparisonResult)compareBoundaryPoints:(HTMLRangeComparisonMethod)method sourceRange:(HTMLRange *)sourceRange
{
CheckValidDocument(self, sourceRange, NSStringFromSelector(_cmd));
switch (method) {
case HTMLRangeComparisonMethodStartToStart:
return CompareBoundaries(_startContainer, _startOffset, sourceRange.startContainer, sourceRange.startOffset);
case HTMLRangeComparisonMethodStartToEnd:
return CompareBoundaries(_endContainer, _endOffset, sourceRange.startContainer, sourceRange.startOffset);
case HTMLRangeComparisonMethodEndToEnd:
return CompareBoundaries(_endContainer, _endOffset, sourceRange.endContainer, sourceRange.endOffset);
case HTMLRangeComparisonMethodEndToStart:
return CompareBoundaries(_startContainer, _startOffset, sourceRange.endContainer, sourceRange.endOffset);
}
}
#pragma mark - Containment
- (NSComparisonResult)comparePoint:(HTMLNode *)node offset:(NSUInteger)offset
{
CheckValidBoundaryNode(_ownerDocument, node, NSStringFromSelector(_cmd));
CheckValidBoundaryNodeType(node, NSStringFromSelector(_cmd));
CheckValidBoundaryOffset(node, offset, NSStringFromSelector(_cmd));
if (CompareBoundaries(node, offset, _startContainer, _startOffset) == NSOrderedAscending) {
return NSOrderedAscending;
}
if (CompareBoundaries(node, offset, _endContainer, _endOffset) == NSOrderedDescending) {
return NSOrderedDescending;
}
return NSOrderedSame;
}
- (BOOL)containsPoint:(HTMLNode *)node offset:(NSUInteger)offset
{
return [self comparePoint:node offset:offset] == NSOrderedSame;
}
- (BOOL)intersectsNode:(HTMLNode *)node
{
if (self.rootNode != node.rootNode) {
return NO;
}
HTMLNode *parent = node.parentNode;
if (parent == nil) {
return YES;
}
NSUInteger offset = node.index;
if (CompareBoundaries(parent, offset, _endContainer, _endOffset) == NSOrderedAscending &&
CompareBoundaries(parent, offset + 1, _startContainer, _startOffset) == NSOrderedDescending) {
return YES;
}
return NO;
}
- (BOOL)containsNode:(HTMLNode *)node
{
return CompareBoundaries(_startContainer, _startOffset, node, 0) == NSOrderedAscending &&
CompareBoundaries(_endContainer, _endOffset, node, node.length) == NSOrderedDescending;
}
- (BOOL)partiallyContainsNode:(HTMLNode *)node
{
return [GetAncestorNodes(_startContainer) containsObject:node] || [GetAncestorNodes(_endContainer) containsObject:node];
}
- (NSArray *)containedNodes:(HTMLNode *)commonAncestor
{
NSMutableArray *containedNodes = [NSMutableArray array];
[commonAncestor.childNodes enumerateObjectsUsingBlock:^(HTMLNode * _Nonnull node, NSUInteger idx, BOOL * _Nonnull stop) {
if (node.nodeType == HTMLNodeDocumentType) {
[NSException raise:HTMLKitHierarchyRequestError format:@"Hierarchy Request Error, encountered a DOCTYPE contained in range: %@", self];
}
if ([self containsNode:node]) {
[containedNodes addObject:node];
}
}];
return containedNodes;
}
#pragma mark - Update Callbacks
- (void)didRemoveCharacterDataInNode:(HTMLCharacterData *)node atOffset:(NSUInteger)offset withLength:(NSUInteger)length
{
if (_startContainer == node && _startOffset > offset) {
if (_startOffset <= offset + length) {
_startOffset = offset;
} else {
_startOffset = _startOffset - length;
}
}
if (_endContainer == node && _endOffset > offset) {
if (_endOffset <= offset + length) {
_endOffset = offset;
} else {
_endOffset = _endOffset - length;
}
}
}
- (void)didAddCharacterDataToNode:(HTMLCharacterData *)node atOffset:(NSUInteger)offset withLength:(NSUInteger)length
{
if (_startContainer == node && _startOffset > offset) {
_startOffset = _startOffset + length;
}
if (_endContainer == node && _endOffset > offset) {
_endOffset = _endOffset + length;
}
}
- (void)didInsertNewTextNode:(HTMLText *)newNode intoParent:(HTMLNode *)parent afterSplittingTextNode:(HTMLText *)node atOffset:(NSUInteger)offset
{
if (_startContainer == node && _startOffset > offset) {
_startContainer = newNode;
_startOffset -= offset;
}
if (_endContainer == node && _endOffset > offset) {
_endContainer = newNode;
_endOffset -= offset;
}
if (_startContainer == parent && _startOffset == node.index + 1) {
_startOffset += 1;
}
if (_endContainer == parent && _endOffset == node.index + 1) {
_endOffset += 1;
}
}
- (void)clampRangesAfterSplittingTextNode:(HTMLText *)node atOffset:(NSUInteger)offset
{
if (_startContainer == node && _startOffset > offset) {
_startOffset = offset;
}
if (_endContainer == node && _endOffset > offset) {
_endOffset = offset;
}
}
- (void)runRemovingStepsForNode:(HTMLNode *)oldNode withOldParent:(HTMLNode *)oldParent andOldPreviousSibling:(HTMLNode *)oldPreviousSibling
{
NSUInteger oldIndex = oldPreviousSibling.index + 1;
if ([_startContainer containsNode:oldNode]) {
[self setStartNode:oldNode startOffset:oldIndex];
}
if ([_endContainer containsNode:oldNode]) {
[self setEndNode:oldNode endOffset:oldIndex];
}
if (_startContainer == oldParent && _startOffset > oldIndex) {
_startOffset -= 1;
}
if (_endContainer == oldParent && _endOffset > oldIndex) {
_endOffset -= 1;
}
}
#pragma mark - Mutations
NS_INLINE HTMLNode * GetHighestPartiallyContainedChild(HTMLNode *node, HTMLNode *root)
{
if (node == root) {
return nil;
}
while (node.parentNode != root) {
node = node.parentNode;
}
return node;
}
NS_INLINE HTMLCharacterData * CloneCharachterData(HTMLNode *node, NSUInteger start, NSUInteger length, BOOL delete)
{
HTMLCharacterData *clone = (HTMLCharacterData *)[node copy];
NSRange range = NSMakeRange(start, length);
[clone setData:[clone.data substringWithRange:range]];
if (delete) {
[(HTMLCharacterData *)node deleteDataInRange:range];
}
return clone;
}
- (void)deleteContents
{
if (self.isCollapsed) {
return;
}
if (_startContainer == _endContainer && [_startContainer isKindOfClass:[HTMLCharacterData class]]) {
[(HTMLCharacterData *)_startContainer deleteDataInRange:NSMakeRange(_startOffset, _endOffset - _startOffset)];
return;
}
HTMLNode *commonAncestor = self.commonAncestorContainer;
NSMutableArray *containedNodes = [NSMutableArray array];
HTMLNode *node = FollowingNode(_startContainer, commonAncestor);
while (node) {
if ([self containsNode:node]) {
[containedNodes addObject:node];
node = FollowingNodeSkippingChildren(node, commonAncestor);
} else {
node = FollowingNode(node, commonAncestor);
}
}
HTMLNode *newNode = _startContainer;
NSUInteger newOffset = _startOffset;
if (![_startContainer containsNode:_endContainer]) {
HTMLNode *referenceNode = _startContainer;
while (referenceNode.parentNode) {
if ([referenceNode.parentNode containsNode:_endContainer]) {
newNode = referenceNode.parentNode;
newOffset = referenceNode.index + 1;
break;
}
referenceNode = referenceNode.parentNode;
}
}
if ([_startContainer isKindOfClass:[HTMLCharacterData class]]) {
[(HTMLCharacterData *)_startContainer deleteDataInRange:NSMakeRange(_startOffset, _startContainer.length - _startOffset)];
}
for (HTMLNode *node in containedNodes) {
[node removeFromParentNode];
}
if ([_endContainer isKindOfClass:[HTMLCharacterData class]]) {
[(HTMLCharacterData *)_endContainer deleteDataInRange:NSMakeRange(0, _endOffset)];
}
[self setStartNode:newNode startOffset:newOffset];
[self setEndNode:newNode endOffset:newOffset];
}
- (HTMLDocumentFragment *)extractContents
{
HTMLDocumentFragment *fragment = [[HTMLDocumentFragment alloc] initWithDocument:_ownerDocument];
// Nothing todo
if (self.isCollapsed) {
return fragment;
}
// Same character data container, handle that and return
if (_startContainer == _endContainer && [_startContainer isKindOfClass:[HTMLCharacterData class]]) {
HTMLCharacterData *clone = CloneCharachterData(_startContainer, _startOffset, _endOffset - _startOffset, YES);
[fragment appendNode:clone];
return fragment;
}
HTMLNode *commonAncestor = self.commonAncestorContainer;
HTMLNode *firstPartiallyContainedChild = GetHighestPartiallyContainedChild(_startContainer, commonAncestor);
HTMLNode *lastPartiallyContainedChild = GetHighestPartiallyContainedChild(_endContainer, commonAncestor);
NSArray *containedNodes = [self containedNodes:commonAncestor];
HTMLNode *newNode = _startContainer;
NSUInteger newOffset = _startOffset;
if (![_startContainer containsNode:_endContainer]) {
HTMLNode *referenceNode = _startContainer;
while (referenceNode.parentNode) {
if ([referenceNode.parentNode containsNode:_endContainer]) {
newNode = referenceNode.parentNode;
newOffset = referenceNode.index + 1;
break;
}
referenceNode = referenceNode.parentNode;
}
}
if ([firstPartiallyContainedChild isKindOfClass:[HTMLCharacterData class]]) {
HTMLCharacterData *clone = CloneCharachterData(_startContainer, _startOffset, _startContainer.length - _startOffset, YES);
[fragment appendNode:clone];
} else if (firstPartiallyContainedChild != nil) {
HTMLNode *clone = [firstPartiallyContainedChild copy];
[fragment appendNode:clone];
HTMLRange *subRange = [[HTMLRange alloc] initWithDowcument:_ownerDocument
startContainer:_startContainer
startOffset:_startOffset
endContainer:firstPartiallyContainedChild
endOffset:firstPartiallyContainedChild.length];
HTMLDocumentFragment *subFragment = [subRange extractContents];
[clone appendNode:subFragment];
}
for (HTMLNode *node in containedNodes) {
[fragment appendNode:node];
}
if ([lastPartiallyContainedChild isKindOfClass:[HTMLCharacterData class]]) {
HTMLCharacterData *clone = CloneCharachterData(_endContainer, 0, _endOffset, YES);
[fragment appendNode:clone];
} else if (lastPartiallyContainedChild != nil) {
HTMLNode *clone = [lastPartiallyContainedChild copy];
[fragment appendNode:clone];
HTMLRange *subRange = [[HTMLRange alloc] initWithDowcument:_ownerDocument
startContainer:lastPartiallyContainedChild
startOffset:0
endContainer:_endContainer
endOffset:_endOffset];
HTMLDocumentFragment *subFragment = [subRange extractContents];
[clone appendNode:subFragment];
}
[self setStartNode:newNode startOffset:newOffset];
[self setEndNode:newNode endOffset:newOffset];
return fragment;
}
- (HTMLDocumentFragment *)cloneContents
{
HTMLDocumentFragment *fragment = [[HTMLDocumentFragment alloc] initWithDocument:_ownerDocument];
// Nothing todo
if (self.isCollapsed) {
return fragment;
}
// Same character data container, handle that and return
if (_startContainer == _endContainer && [_startContainer isKindOfClass:[HTMLCharacterData class]]) {
HTMLCharacterData *clone = CloneCharachterData(_startContainer, _startOffset, _endOffset - _startOffset, NO);
[fragment appendNode:clone];
return fragment;
}
HTMLNode *commonAncestor = self.commonAncestorContainer;
HTMLNode *firstPartiallyContainedChild = GetHighestPartiallyContainedChild(_startContainer, commonAncestor);
HTMLNode *lastPartiallyContainedChild = GetHighestPartiallyContainedChild(_endContainer, commonAncestor);
NSArray *containedNodes = [self containedNodes:commonAncestor];
if ([firstPartiallyContainedChild isKindOfClass:[HTMLCharacterData class]]) {
HTMLCharacterData *clone = CloneCharachterData(_startContainer, _startOffset, _startContainer.length - _startOffset, NO);
[fragment appendNode:clone];
} else if (firstPartiallyContainedChild != nil) {
HTMLNode *clone = [firstPartiallyContainedChild copy];
[fragment appendNode:clone];
HTMLRange *subRange = [[HTMLRange alloc] initWithDowcument:_ownerDocument
startContainer:_startContainer
startOffset:_startOffset
endContainer:firstPartiallyContainedChild
endOffset:firstPartiallyContainedChild.length];
HTMLDocumentFragment *subFragment = [subRange cloneContents];
[clone appendNode:subFragment];
}
for (HTMLNode *node in containedNodes) {
HTMLNode *clone = [node cloneNodeDeep:YES];
[fragment appendNode:clone];
}
if ([lastPartiallyContainedChild isKindOfClass:[HTMLCharacterData class]]) {
HTMLCharacterData *clone = CloneCharachterData(_endContainer, 0, _endOffset, NO);
[fragment appendNode:clone];
} else if (lastPartiallyContainedChild != nil) {
HTMLNode *clone = [lastPartiallyContainedChild copy];
[fragment appendNode:clone];
HTMLRange *subRange = [[HTMLRange alloc] initWithDowcument:_ownerDocument
startContainer:lastPartiallyContainedChild
startOffset:0
endContainer:_endContainer
endOffset:_endOffset];
HTMLDocumentFragment *subFragment = [subRange cloneContents];
[clone appendNode:subFragment];
}
return fragment;
}
#pragma mark - Insertion & Surround
NS_INLINE void CheckValidInsertionNode(HTMLNode *startContainer, HTMLNode *node, NSString *cmd)
{
if (startContainer == node || startContainer.nodeType == HTMLNodeComment ||
(startContainer.nodeType == HTMLNodeText && startContainer.parentNode == nil)) {
[NSException raise:HTMLKitHierarchyRequestError
format:@"%@: Hierarchy Request Error, cannot insert node into range: %@", cmd, node];
}
}
- (void)insertNode:(HTMLNode *)node
{
CheckValidInsertionNode(_startContainer, node, NSStringFromSelector(_cmd));
HTMLNode *referenceNode = nil;
if (_startContainer.nodeType == HTMLNodeText) {
referenceNode = _startContainer;
} else {
referenceNode = [_startContainer childNodeAtIndex:_startOffset];
}
HTMLNode *parent = _startContainer;
if (referenceNode != nil) {
parent = referenceNode.parentNode;
}
if (_startContainer.nodeType == HTMLNodeText) {
referenceNode = [(HTMLText *)_startContainer splitTextAtOffset:_startOffset];
}
if (node == referenceNode) {
referenceNode = referenceNode.nextSibling;
}
[node removeFromParentNode];
NSUInteger newOffset = referenceNode ? referenceNode.index : parent.length;
newOffset += (node.nodeType == HTMLNodeDocumentFragment) ? node.length : 1;
[parent insertNode:node beforeChildNode:referenceNode];
if (self.isCollapsed) {
[self setEndNode:parent endOffset:newOffset];
}
}
NS_INLINE void CheckValidSurroundState(HTMLRange *range, NSString *cmd)
{
for (HTMLNode *node in GetAncestorNodes(range.startContainer)) {
if ([node containsNode:range.endContainer]) {
return;
}
if (node.nodeType != HTMLNodeText) {
[NSException raise:HTMLKitInvalidStateError
format:@"%@: Invalid State Error, cannot surround range with a partially-contaied non-text node.", cmd];
}
};
for (HTMLNode *node in GetAncestorNodes(range.endContainer)) {
if ([node containsNode:range.startContainer]) {
return;
}
if (node.nodeType != HTMLNodeText) {
[NSException raise:HTMLKitInvalidNodeTypeError
format:@"%@: Invalid State Error, cannot surround range with a partially-contaied non-text node.", cmd];
}
};
}
NS_INLINE void CheckValidSurroundNodeType(HTMLNode *node, NSString *cmd)
{
if (node == nil || node.nodeType == HTMLNodeDocumentType || node.nodeType == HTMLNodeDocument ||
node.nodeType == HTMLNodeDocumentFragment) {
[NSException raise:HTMLKitInvalidNodeTypeError
format:@"%@: Invalid Node Type Error, %@ is not a valid new parent for a range.",
cmd, node];
}
}
- (void)surroundContents:(HTMLNode *)newParent
{
CheckValidSurroundState(self, NSStringFromSelector(_cmd));
CheckValidSurroundNodeType(newParent, NSStringFromSelector(_cmd));
HTMLDocumentFragment *fragment = [self extractContents];
[newParent removeAllChildNodes];
[self insertNode:newParent];
[newParent appendNode:fragment];
[self selectNode:newParent];
}
#pragma mark - Description & Stringifier
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@: %p (%@, %lu), (%@, %lu)>", self.class, self,
_startContainer, (unsigned long)_startOffset,
_endContainer, (unsigned long)_endOffset];
}
- (NSString *)textContent
{
HTMLNode *lastNode = nil;
if ([_endContainer isKindOfClass:[HTMLCharacterData class]]) {
lastNode = FollowingNodeSkippingChildren(_endContainer, _ownerDocument);
} else if (_endContainer.childNodesCount > _endOffset) {
lastNode = [_endContainer childNodeAtIndex:_endOffset];
} else {
lastNode = FollowingNodeSkippingChildren(_endContainer, _ownerDocument);
}
NSMutableString *content = [NSMutableString string];
for (HTMLNode *node = _startContainer; node != lastNode; node = FollowingNode(node, _ownerDocument)) {
if (node.nodeType == HTMLNodeText) {
HTMLText *text = (HTMLText *)node;
if (node == _startContainer) {
NSString *string = [text substringDataWithRange:NSMakeRange(_startOffset, _startContainer.length - _startOffset)];
[content appendString:string];
} else if (node == _endContainer) {
NSString *string = [text substringDataWithRange:NSMakeRange(0, _endOffset)];
[content appendString:string];
} else {
[content appendString:text.data];
}
}
}
return content;
}
@end