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.
This commit is contained in:
iska
2015-04-04 02:30:08 +02:00
parent 5a140220b1
commit d1a4aeb5e1
6 changed files with 97 additions and 43 deletions
+1
View File
@@ -39,5 +39,6 @@ typedef void (^ HTMLStreamReaderErrorCallback)(NSString *reason);
- (void)markCurrentLocation;
- (void)rewindToMarkedLocation;
- (void)reset;
@end
+8
View File
@@ -225,4 +225,12 @@
_consume = 0;
}
- (void)reset
{
_mark = 0;
_location = 0;
_scanner.scanLocation = 0;
_consume = 0;
}
@end
+6 -1
View File
@@ -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
+72 -42
View File
@@ -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;
+2
View File
@@ -24,4 +24,6 @@
- (instancetype)initWithString:(NSString *)string;
- (void)reset;
@end
+8
View File
@@ -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) {