From d1a4aeb5e1f4f1dbead144a89e5ab8188afc558f Mon Sep 17 00:00:00 2001 From: iska Date: Sat, 4 Apr 2015 02:30:08 +0200 Subject: [PATCH] Add HTML Parser methods for parsing a document and document fragments The parser is initialized with a HTML string which can be parsed as a document or a document fragment. When parsing a fragment a context element should be provided. It is also possible to parse the same string as a fragment for different context elements. In this case the parser reset its internal state and runs the parsing algorithm again. Parsing a fragment for the same context element runs the algorithm only once, since the parser caches the context element and the parsing results. --- HTMLKit/HTMLInputStreamReader.h | 1 + HTMLKit/HTMLInputStreamReader.m | 8 +++ HTMLKit/HTMLParser.h | 7 +- HTMLKit/HTMLParser.m | 114 ++++++++++++++++++++------------ HTMLKit/HTMLTokenizer.h | 2 + HTMLKit/HTMLTokenizer.m | 8 +++ 6 files changed, 97 insertions(+), 43 deletions(-) diff --git a/HTMLKit/HTMLInputStreamReader.h b/HTMLKit/HTMLInputStreamReader.h index 1cc77f9..1ffdce8 100644 --- a/HTMLKit/HTMLInputStreamReader.h +++ b/HTMLKit/HTMLInputStreamReader.h @@ -39,5 +39,6 @@ typedef void (^ HTMLStreamReaderErrorCallback)(NSString *reason); - (void)markCurrentLocation; - (void)rewindToMarkedLocation; +- (void)reset; @end diff --git a/HTMLKit/HTMLInputStreamReader.m b/HTMLKit/HTMLInputStreamReader.m index e69b83f..72b4644 100644 --- a/HTMLKit/HTMLInputStreamReader.m +++ b/HTMLKit/HTMLInputStreamReader.m @@ -225,4 +225,12 @@ _consume = 0; } +- (void)reset +{ + _mark = 0; + _location = 0; + _scanner.scanLocation = 0; + _consume = 0; +} + @end diff --git a/HTMLKit/HTMLParser.h b/HTMLKit/HTMLParser.h index 642f0f0..5f29bfa 100644 --- a/HTMLKit/HTMLParser.h +++ b/HTMLKit/HTMLParser.h @@ -11,7 +11,12 @@ @interface HTMLParser : NSObject +@property (nonatomic, strong, readonly) NSArray *parseErrors; +@property (nonatomic, strong, readonly) HTMLDocument *document; -- (instancetype)initWithString:(NSString *)string context:(HTMLElement *)context; +- (instancetype)initWithString:(NSString *)string; + +- (HTMLDocument *)parseDocument; +- (NSArray *)parseFragmentWithContextElement:(HTMLElement *)contextElement; @end diff --git a/HTMLKit/HTMLParser.m b/HTMLKit/HTMLParser.m index 1106c52..d453490 100644 --- a/HTMLKit/HTMLParser.m +++ b/HTMLKit/HTMLParser.m @@ -60,12 +60,14 @@ #pragma mark - Lifecycle -- (instancetype)initWithString:(NSString *)string context:(HTMLElement *)context +- (instancetype)initWithString:(NSString *)string { self = [super init]; if (self) { - _contextElement = context; - _fragmentParsingAlgorithm = (context != nil); + _framesetOkFlag = YES; + _fragmentParsingAlgorithm = NO; + _fosterParenting = NO; + _ignoreNextLineFeedCharacterToken = NO; _errors = [NSMutableArray new]; @@ -83,11 +85,6 @@ _headElementPointer = nil; _formElementPointer = nil; - - _framesetOkFlag = YES; - _fragmentParsingAlgorithm = NO; - _fosterParenting = NO; - _ignoreNextLineFeedCharacterToken = NO; } return self; } @@ -110,50 +107,83 @@ - (HTMLDocument *)document { - if (_document == nil) { - [self parse]; - } - return _document; + return _document ?: [self parseDocument]; } #pragma mark - Parse -- (void)parse +- (void)initializeDocument { - _document = [HTMLDocument new]; - if (_fragmentParsingAlgorithm) { - if (_contextElement != nil) { - if ([_contextElement.tagName isEqualToAny:@"title", @"textarea", nil]) { - _tokenizer.state = HTMLTokenizerStateRCDATA; - } else if ([_contextElement.tagName isEqualToAny:@"style", @"xmp", @"iframe", @"noembed", @"noframes", nil]) { - _tokenizer.state = HTMLTokenizerStateRAWTEXT; - } else if ([_contextElement.tagName isEqualToString:@"script"]) { - _tokenizer.state = HTMLTokenizerStateScriptData; - } else if ([_contextElement.tagName isEqualToString:@"noscript"]) { - _tokenizer.state = HTMLTokenizerStateRAWTEXT; - } else if ([_contextElement.tagName isEqualToString:@"plaintext"]) { - _tokenizer.state = HTMLTokenizerStatePLAINTEXT; - } else { - _tokenizer.state = HTMLTokenizerStateData; - } - } + if (_document == nil) { + _document = [HTMLDocument new]; + } + _document.quirksMode = HTMLQuirksModeNoQuirks; + _document.documentType = nil; + [_document removeAllChildNodes]; - HTMLElement *root = [[HTMLElement alloc] initWithTagName:@"html"]; - [_document appendNode:root]; - [_stackOfOpenElements pushElement:root]; + _fragmentParsingAlgorithm = NO; +} - if ([_contextElement.tagName isEqualToString:@"template"]) { -#warning Implement HTML Template - } +- (HTMLDocument *)parseDocument +{ + [self initializeDocument]; + [self runParser]; + return _document; +} - [self resetInsertionModeAppropriately]; - - _formElementPointer = _contextElement; - while (_formElementPointer != nil && ![_formElementPointer.tagName isEqualToString:@"form"]) { - _formElementPointer = _formElementPointer.parentElement; - } +- (NSArray *)parseFragmentWithContextElement:(HTMLElement *)contextElement +{ + if (contextElement == nil) { + return nil; } + if ([_contextElement isEqualTo:contextElement]) { + return [_document childNodeAtIndex:0].childNodes.array; + } + + [self initializeDocument]; + [_tokenizer reset]; + _contextElement = contextElement; + _fragmentParsingAlgorithm = YES; + + _document.quirksMode = _contextElement.ownerDocument ? _contextElement.ownerDocument.quirksMode : HTMLQuirksModeNoQuirks; + + if ([_contextElement.tagName isEqualToAny:@"title", @"textarea", nil]) { + _tokenizer.state = HTMLTokenizerStateRCDATA; + } else if ([_contextElement.tagName isEqualToAny:@"style", @"xmp", @"iframe", @"noembed", @"noframes", nil]) { + _tokenizer.state = HTMLTokenizerStateRAWTEXT; + } else if ([_contextElement.tagName isEqualToString:@"script"]) { + _tokenizer.state = HTMLTokenizerStateScriptData; + } else if ([_contextElement.tagName isEqualToString:@"noscript"]) { + _tokenizer.state = HTMLTokenizerStateRAWTEXT; + } else if ([_contextElement.tagName isEqualToString:@"plaintext"]) { + _tokenizer.state = HTMLTokenizerStatePLAINTEXT; + } else { + _tokenizer.state = HTMLTokenizerStateData; + } + + HTMLElement *root = [[HTMLElement alloc] initWithTagName:@"html"]; + [_document appendNode:root]; + [_stackOfOpenElements pushElement:root]; + + if ([_contextElement.tagName isEqualToString:@"template"]) { +#warning Implement HTML Template + } + + [self resetInsertionModeAppropriately]; + + _formElementPointer = _contextElement; + while (_formElementPointer != nil && ![_formElementPointer.tagName isEqualToString:@"form"]) { + _formElementPointer = _formElementPointer.parentElement; + } + + [self runParser]; + + return root.childNodes.array; +} + +- (void)runParser +{ for (HTMLToken *token in _tokenizer) { if (_document.readyState == HTMLDocumentComplete) { break; diff --git a/HTMLKit/HTMLTokenizer.h b/HTMLKit/HTMLTokenizer.h index 359404d..15ce13c 100644 --- a/HTMLKit/HTMLTokenizer.h +++ b/HTMLKit/HTMLTokenizer.h @@ -24,4 +24,6 @@ - (instancetype)initWithString:(NSString *)string; +- (void)reset; + @end diff --git a/HTMLKit/HTMLTokenizer.m b/HTMLKit/HTMLTokenizer.m index bc7ac3f..d8f3cd9 100644 --- a/HTMLKit/HTMLTokenizer.m +++ b/HTMLKit/HTMLTokenizer.m @@ -83,6 +83,14 @@ #pragma mark - State Machine +- (void)reset +{ + _currentState = HTMLTokenizerStateData; + [_tokens removeAllObjects]; + [_inputStreamReader reset]; + _eof = NO; +} + - (id)nextObject { while (_eof == NO && _tokens.count == 0) {