diff --git a/HTMLKit/HTMLParser.m b/HTMLKit/HTMLParser.m index 303b923..04266eb 100644 --- a/HTMLKit/HTMLParser.m +++ b/HTMLKit/HTMLParser.m @@ -964,6 +964,9 @@ case HTMLTokenTypeStartTag: [self processStartTagTokenInBody:token.asStartTagToken]; return; + case HTMLTokenTypeEndTag: + [self processEndTagTokenInBody:token.asEndTagToken]; + return; default: break; } @@ -1114,7 +1117,7 @@ if (element != nil) { [self emitParseError:@"Unexpected nested Start Tag (a) in
"]; if ([self runAdoptionAgencyAlgorithmForTagName:@"a"]) { -#warning Handle Any Other End Tag Token + [self processAnyOtherEndTagTokenInBody:token]; return; } [_listOfActiveFormattingElements removeObject:element]; @@ -1133,7 +1136,7 @@ if ([_stackOfOpenElements hasElementInScopeWithTagName:@"nobr"]) { [self emitParseError:@"Unexpected nested Start Tag (nobr) in "]; if ([self runAdoptionAgencyAlgorithmForTagName:@"nobr"]) { -#warning Handle Any Other End Tag Token + [self processAnyOtherEndTagTokenInBody:token]; return; } [self reconstructActiveFormattingElements]; @@ -1142,7 +1145,7 @@ } else if ([token.tagName isEqualToAny:@"a", @"b", @"big", @"code", @"em", @"font", @"i", @"nobr", @"s", @"small", @"strike", @"strong", @"tt", @"u", nil]) { if ([self runAdoptionAgencyAlgorithmForTagName:tagName]) { -#warning Handle Any Other End Tag Token + [self processAnyOtherEndTagTokenInBody:token]; return; } } else if ([tagName isEqualToAny:@"applet", @"marquee", nil]) { @@ -1299,6 +1302,145 @@ } } +- (void)processEndTagTokenInBody:(HTMLEndTagToken *)token +{ + NSString *tagName = token.tagName; + + if ([tagName isEqualToString:@"template"]) { +#warning Implement HTML Template + } else if ([tagName isEqualToAny:@"body", @"html", nil]) { + // End tags "body" & "html" are identical, expect for the reprocessing step + if (![_stackOfOpenElements hasElementInScopeWithTagName:@"body"]) { + [self emitParseError:@"End Tag (body) without body element in scope in "]; + } + for (HTMLElement *node in _stackOfOpenElements) { + if ([node.tagName isEqualToAny:@"dd", @"dt", @"li", @"optgroup", @"option", @"p", @"rp" + @"rt", @"tbody", @"td", @"tfoot", @"th", @"thead", @"tr", @"body", @"html", nil]) { + [self emitParseError:@"End Tag (%@) with open element (%@) in ", tagName, node.tagName]; + break; + } + } + [self switchInsertionMode:HTMLInsertionModeAfterBody]; + if ([tagName isEqualToString:@"html"]) { + [self reprocessToken:token]; + } + } else if ([tagName isEqualToAny:@"address", @"article", @"aside", @"blockquote", @"button", + @"center", @"details", @"dialog", @"dir", @"div", @"dl", @"fieldset", @"figcaption", + @"figure", @"footer", @"header", @"hgroup", @"listing", @"main", @"menu", @"nav", + @"ol", @"pre", @"section", @"summary", @"ul", nil]) { + if ([_stackOfOpenElements hasElementInScopeWithTagName:tagName]) { + [self emitParseError:@"End Tag (%@) with open element in ", tagName]; + return; + } + [self generateImpliedEndTagsExceptForElement:nil]; + if (![self.currentNode.tagName isEqualToString:tagName]) { + [self emitParseError:@"Unexpected End Tag Token (%@) in ", tagName]; + } + [_stackOfOpenElements popElementsUntilElementPoppedWithTagName:tagName]; + } else if ([tagName isEqualToString:@"form"]) { +#warning Implement HTML Template + HTMLElement *node = _formElementPointer; + _formElementPointer = nil; + if (node == nil || ![_stackOfOpenElements hasElementInScopeWithTagName:node.tagName]) { + [self emitParseError:@"Unexpected closed (form) element in "]; + return; + } + [self generateImpliedEndTagsExceptForElement:nil]; + if ([self.currentNode isEqual:node]) { + [self emitParseError:@"Unexpected nested (form) element in "]; + } + [_stackOfOpenElements removeElement:node]; + } else if ([tagName isEqualToString:@"p"]) { + if (![_stackOfOpenElements hasElementInButtonScopeWithTagName:@"p"]) { + [self emitParseError:@"Unexpected End Tag Token (p) in "]; + HTMLEndTagToken *pToken = [[HTMLEndTagToken alloc] initWithTagName:@"p"]; + [self insertElementForToken:pToken]; + } + [self closePElement]; + } else if ([tagName isEqualToString:@"li"]) { + if (![_stackOfOpenElements hasElementInScopeWithTagName:@"li"]) { + [self emitParseError:@"Unexpected closed (li) element in "]; + return; + } + [self generateImpliedEndTagsExceptForElement:nil]; + if ([self.currentNode.tagName isEqualToString:@"li"]) { + [self emitParseError:@"Unexpected nested (li) element in "]; + } + [_stackOfOpenElements popElementsUntilElementPoppedWithTagName:@"li"]; + } else if ([tagName isEqualToAny:@"dd", @"dt", nil]) { + if (![_stackOfOpenElements hasElementInScopeWithTagName:tagName]) { + [self emitParseError:@"Unexpected closed (%@) element in ", tagName]; + return; + } + [self generateImpliedEndTagsExceptForElement:tagName]; + if ([self.currentNode.tagName isEqualToString:@"li"]) { + [self emitParseError:@"Unexpected nested (%@) element in ", tagName]; + } + [_stackOfOpenElements popElementsUntilElementPoppedWithTagName:tagName]; + } else if ([tagName isEqualToAny:@"h1", @"h2", @"h3", @"h4", @"h5", @"h6", nil]) { + if (![_stackOfOpenElements hasAnyElementInScopeWithAnyOfTagNames:@[@"h1", @"h2", @"h3", @"h4", @"h5", @"h6"]]) { + [self emitParseError:@"Unexpected closed (%@) element in ", tagName]; + return; + } + [self generateImpliedEndTagsExceptForElement:nil]; + if (![self.currentNode.tagName isEqualToAny:@"h1", @"h2", @"h3", @"h4", @"h5", @"h6", nil]) { + [self emitParseError:@"Unexpected nested (%@) element in ", tagName]; + } + [_stackOfOpenElements popElementsUntilAnElementPoppedWithAnyOfTagNames:@[@"h1", @"h2", @"h3", @"h4", @"h5", @"h6"]]; + } else if ([tagName isEqualToString:@"sarcasm"]) { + // Taking a Deep Breath + [self processAnyOtherEndTagTokenInBody:token]; + return; + } else if ([tagName isEqualToAny:@"a", @"b", @"big", @"code", @"em", @"font", @"i", @"nobr", @"s", @"small", @"strike", + @"strong", @"tt", @"u", nil]) { + if ([self runAdoptionAgencyAlgorithmForTagName:tagName]) { + [self processAnyOtherEndTagTokenInBody:token]; + return; + } + } else if ([tagName isEqualToAny:@"applet", @"marquee", @"object", nil]) { + if (![_stackOfOpenElements hasAnyElementInScopeWithAnyOfTagNames:@[@"applet", @"marquee", @"object"]]) { + [self emitParseError:@"Unexpected closed (%@) element in ", tagName]; + return; + } + [self generateImpliedEndTagsExceptForElement:nil]; + if (![self.currentNode.tagName isEqualToAny:@"applet", @"marquee", @"object", nil]) { + [self emitParseError:@"Unexpected nested (%@) element in ", tagName]; + } + [_stackOfOpenElements popElementsUntilAnElementPoppedWithAnyOfTagNames:@[@"applet", @"marquee", @"object"]]; + while (![_listOfActiveFormattingElements.lastObject isEqual:[HTMLMarker marker]]) { + [_listOfActiveFormattingElements removeLastObject]; + } + [_listOfActiveFormattingElements removeLastObject]; + } else if ([tagName isEqualToString:@"br"]) { + [self emitParseError:@"Unexpected End Tag Token (br) in "]; + HTMLStartTagToken *brToken = [[HTMLStartTagToken alloc] initWithTagName:@"br"]; + [self processStartTagTokenInBody:brToken]; + } else { + [self processAnyOtherEndTagTokenInBody:token]; + } +} + +- (void)processAnyOtherEndTagTokenInBody:(HTMLEndTagToken *)token +{ + HTMLElement *node = _stackOfOpenElements.currentNode; + NSUInteger index = _stackOfOpenElements.count - 1; + + while (YES) { + if ([node.tagName isEqualToString:token.tagName]) { + [self generateImpliedEndTagsExceptForElement:token.tagName]; + if (![node.tagName isEqualToString:self.currentNode.tagName]) { + [self emitParseError:@"Unexpected nested End Tag Token (%@) in ", node.tagName]; + } + [_stackOfOpenElements popElementsUntilElementPopped:node]; + break; + } else if (IsSpecialElement(node)) { + [self emitParseError:@"Unexpected End Tag Token (%@) in ", node.tagName]; + return; + } + node = _stackOfOpenElements[--index]; + } +} + - (void)HTMLInsertionModeText:(HTMLToken *)token { diff --git a/HTMLKit/HTMLStackOfOpenElements.h b/HTMLKit/HTMLStackOfOpenElements.h index 4946e8c..9588b6d 100644 --- a/HTMLKit/HTMLStackOfOpenElements.h +++ b/HTMLKit/HTMLStackOfOpenElements.h @@ -28,11 +28,13 @@ - (void)popCurrentNode; - (void)popElementsUntilElementPoppedWithTagName:(NSString *)tagName; +- (void)popElementsUntilAnElementPoppedWithAnyOfTagNames:(NSArray *)tagNames; - (void)popElementsUntilElementPopped:(HTMLElement *)element; - (void)popAll; - (HTMLElement *)hasElementInScopeWithTagName:(NSString *)tagName; +- (HTMLElement *)hasAnyElementInScopeWithAnyOfTagNames:(NSArray *)tagNames; - (HTMLElement *)hasElementInListItemScopeWithTagName:(NSString *)tagName; - (HTMLElement *)hasElementInButtonScopeWithTagName:(NSString *)tagName; - (HTMLElement *)hasElementInTableScopeWithTagName:(NSString *)tagName; diff --git a/HTMLKit/HTMLStackOfOpenElements.m b/HTMLKit/HTMLStackOfOpenElements.m index 0e90ba8..7a0ada9 100644 --- a/HTMLKit/HTMLStackOfOpenElements.m +++ b/HTMLKit/HTMLStackOfOpenElements.m @@ -120,6 +120,14 @@ [_stack removeLastObject]; } +- (void)popElementsUntilAnElementPoppedWithAnyOfTagNames:(NSArray *)tagNames +{ + while (![tagNames containsObject:self.currentNode.tagName]) { + [_stack removeLastObject]; + } + [_stack removeLastObject]; +} + - (void)popElementsUntilElementPopped:(HTMLElement *)element { while (![self.currentNode isEqual:element]) { @@ -137,7 +145,12 @@ - (HTMLElement *)hasElementInScopeWithTagName:(NSString *)tagName; { - return [self hasElementInSpecificScopeWithTagName:tagName andElementTypes:_specificScopeElementTypes]; + return [self hasAnyElementInSpecificScopeWithTagNames:@[tagName] andElementTypes:_specificScopeElementTypes]; +} + +- (HTMLElement *)hasAnyElementInScopeWithAnyOfTagNames:(NSArray *)tagNames +{ + return [self hasAnyElementInSpecificScopeWithTagNames:tagNames andElementTypes:_specificScopeElementTypes]; } - (HTMLElement *)hasElementInListItemScopeWithTagName:(NSString *)tagName @@ -167,25 +180,17 @@ @"template": @(HTMLNamespaceHTML)}]; } -- (HTMLElement *)hasElementInSelectScopeWithTagName:(NSString *)tagName -{ - for (HTMLElement *node in _stack.reverseObjectEnumerator) { - if ([node.tagName isEqualToString:tagName]) { - return node; - } - if (!(node.namespace == HTMLNamespaceHTML && - [node.tagName isEqualToAny:@"optgroup", @"option", nil])) { - return nil; - } - } - return nil; -} - - (HTMLElement *)hasElementInSpecificScopeWithTagName:(NSString *)tagName andElementTypes:(NSDictionary *)elementTypes +{ + return [self hasAnyElementInSpecificScopeWithTagNames:@[tagName] andElementTypes:elementTypes]; +} + +- (HTMLElement *)hasAnyElementInSpecificScopeWithTagNames:(NSArray *)tagNames + andElementTypes:(NSDictionary *)elementTypes { for (HTMLElement *node in _stack.reverseObjectEnumerator) { - if ([node.tagName isEqualToString:tagName]) { + if ([tagNames containsObject:node.tagName]) { return node; } if ([elementTypes[node.tagName] isEqual:@(node.namespace)]) {