|
//
// GONMarkupParser.m
// GONMarkupParserSample
//
// Created by Nicolas Goutaland on 25/06/14.
// Copyright (c) 2014 Nicolas Goutaland All rights reserved.
//
#import "GONMarkupParser.h"
#import "GONMarkup+Private.h"
#import "GONMarkupDefaultMarkups.h"
#import "GONMarkupParserUtils.h"
#define MARKUP_REGEX @"(.*?)(<[^>]+>|\\Z)"
#define ATTRIBUTES_REGEX @"([^\\s=]+)\\s*=\\s*('(\\\\'|[^']*')|\"(\\\\\"|[^\"])*\")"
#define LOG_IF_DEBUG(level, msg, ...) do {if (_logLevel & level) { NSLog(@"MarkupParser : %@", [NSString stringWithFormat:msg, ##__VA_ARGS__]); }} while(0)
@interface GONMarkupParser ()
// Style
@property (nonatomic, strong) NSMutableDictionary *defaultConfiguration; // Default attributed string configuration
// Fonts
@property (nonatomic, strong) NSMutableDictionary *dicRegisteredFonts; // Registered fonts
// Data
@property (nonatomic, strong) NSMutableDictionary *dicCurrentMarkup; // Dictionary representation of markups
@property (nonatomic, strong) NSRegularExpression *markupRegex; // Regular expression to extract tokens
@property (nonatomic, strong) NSRegularExpression *attributesRegex; // Attributes regex
// Ephemeral internal data. Used to reduce parameters count in internal methods
@property (nonatomic, strong) NSMutableArray *configurationsStack; // Configurations stack
@property (nonatomic, strong) NSMutableArray *markupsStack; // Markups stack
@property (nonatomic, strong) NSMutableArray *markupAttributesStack; // Markups attributes stack
@property (nonatomic, strong) NSMutableDictionary *currentContext; // Current context
@end
@implementation GONMarkupParser
#pragma mark - Constructor
+ (GONMarkupParser *)defaultMarkupParser
{
GONMarkupParser *parser = [[GONMarkupParser alloc] init];
[parser addMarkup:[GONMarkupItalic italicMarkup]];
[parser addMarkup:[GONMarkupBold boldMarkup]];
[parser addMarkup:[GONMarkupDec decMarkup]];
[parser addMarkup:[GONMarkupInc incMarkup]];
[parser addMarkup:[GONMarkupAnchor anchorMarkup]];
[parser addMarkup:[GONMarkupFont fontMarkup]];
[parser addMarkup:[GONMarkupColor colorMarkup]];
[parser addMarkup:[GONMarkupLineBreak lineBreakMarkup]];
[parser addMarkup:[GONMarkupReset resetMarkup]];
[parser addMarkup:[GONMarkupParagrap paragraphMarkup]];
[parser addMarkups:[GONMarkupLineStyle allMarkups]];
[parser addMarkups:[GONMarkupTextStyle allMarkups]];
[parser addMarkups:[GONMarkupList allMarkups]];
[parser addMarkups:[GONMarkupAlignment allMarkups]];
return parser;
}
+ (GONMarkupParser *)emptyMarkupParser
{
return [[GONMarkupParser alloc] init];
}
- (id)init
{
if (self = [super init])
{
_markupRegex = [[NSRegularExpression alloc] initWithPattern:MARKUP_REGEX
options:NSRegularExpressionDotMatchesLineSeparators
error:nil];
_attributesRegex = [[NSRegularExpression alloc] initWithPattern:ATTRIBUTES_REGEX
options:NSRegularExpressionDotMatchesLineSeparators
error:nil];
_assertOnError = NO;
_logLevel = GONMarkupParserLogLevelNone;
_defaultConfiguration = [[NSMutableDictionary alloc] init];
_dicCurrentMarkup = [[NSMutableDictionary alloc] init];
_dicRegisteredFonts = [[NSMutableDictionary alloc] init];
_replaceNewLineCharactersFromInputString = NO;
_replaceHTMLCharactersFromOutputString = YES;
}
return self;
}
#pragma mark - Markup management
- (void)addMarkup:(GONMarkup *)markup
{
// Nothing to do if already added to parser
if (markup.parser == self)
return;
if (markup.parser != nil)
@throw @"Error, a Markup can be used by only one parser at a time";
// Bind to parser
markup.parser = self;
[_dicCurrentMarkup setObject:markup
forKey:markup.tag];
}
- (GONMarkup *)markupForTag:(NSString *)tag
{
// Retrieve markup
return [_dicCurrentMarkup objectForKey:tag];
}
- (void)addMarkups:(id <NSFastEnumeration>)markups
{
for (GONMarkup *markup in markups)
[self addMarkup:markup];
}
- (void)removeMarkup:(GONMarkup *)markup
{
GONMarkup *currentMarkup = [_dicCurrentMarkup objectForKey:markup.tag];
if (currentMarkup == markup)
{
// Remove parser link
markup.parser = nil;
[_dicCurrentMarkup removeObjectForKey:markup.tag];
}
}
- (void)removeMarkups:(id <NSFastEnumeration>)markups
{
for (GONMarkup *markup in markups)
[self removeMarkup:markup];
}
- (void)removeAllMarkups
{
for (GONMarkup *markup in [_dicCurrentMarkup allValues])
markup.parser = nil;
[_dicCurrentMarkup removeAllObjects];
}
#pragma mark - Parser
- (NSMutableAttributedString *)attributedStringFromString:(NSString *)string
{
return [self attributedStringFromString:string
error:nil];
}
- (NSMutableAttributedString *)attributedStringFromString:(NSString *)string error:(NSError **)error
{
LOG_IF_DEBUG(GONMarkupParserLogLevelWorkflow, @"Input string :\n%@\n", string);
// Check for nil values
if (!string)
{
LOG_IF_DEBUG(GONMarkupParserLogLevelWorkflow, @"Input string was <nil>, returning empty string");
return [[NSMutableAttributedString alloc] init];
}
// Make input string mutable
NSMutableString *inputString = [string mutableCopy];
// Handle pre processing
LOG_IF_DEBUG(GONMarkupParserLogLevelWorkflow, @"Preprocessing string");
if (_preProcessingBlock)
_preProcessingBlock(inputString);
// Replace new line characters
if (_replaceNewLineCharactersFromInputString)
inputString = [[[inputString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:@" "] mutableCopy];
// Parse input string
NSMutableAttributedString *resString = [self parseString:inputString error:error];
if (error)
LOG_IF_DEBUG(GONMarkupParserLogLevelWorkflow, @"Parsing completed with an error <%@>", *error);
else
LOG_IF_DEBUG(GONMarkupParserLogLevelWorkflow, @"Parsing completed without error");
// Replace html entities
if (_replaceHTMLCharactersFromOutputString)
[GONMarkupParserUtils cleanHTMLEntitiesFromString:resString.mutableString];
// Handle post processing
LOG_IF_DEBUG(GONMarkupParserLogLevelWorkflow, @"Postprocessing string");
if (_postProcessingBlock)
_postProcessingBlock(resString);
LOG_IF_DEBUG(GONMarkupParserLogLevelWorkflow, @"Result string :\n%@\n", resString);
return resString;
}
- (NSMutableAttributedString *)parseString:(NSString *)inputString error:(NSError **)error
{
// Init stack
_configurationsStack = [[NSMutableArray alloc] init];
_currentContext = [[NSMutableDictionary alloc] init];
_markupsStack = [[NSMutableArray alloc] init];
_markupAttributesStack = [[NSMutableArray alloc] init];
// Parse string
NSArray *results = [_markupRegex matchesInString:inputString
options:0
range:NSMakeRange(0, inputString.length)];
// Prepare result string
NSMutableAttributedString *resultString = [[NSMutableAttributedString alloc] init];
[self handleOpeningTag:GONMarkupFont_TAG error:error];
[self handleOpeningTag:GONMarkupColor_TAG error:error];
// Browse chunks
NSString *tag;
BOOL autoclosingMarkup;
[resultString beginEditing];
for (NSTextCheckingResult *result in results)
{
// Split string
NSArray *parts = [[inputString substringWithRange:result.range] componentsSeparatedByString:@"<"];
// Append extracted string
[resultString appendAttributedString:[self computeFinalExtractedString:[parts firstObject]]];
// Check if a tag was found
if (parts.count > 1)
{
// Extract tag and clean it
tag = [parts objectAtIndex:1];
tag = [tag substringToIndex:tag.length - 1]; // Remove final >
tag = [tag stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([tag rangeOfString:@"/"].location == 0)
{
// Lowercase tag, trim closing character
tag = [[tag substringFromIndex:1] lowercaseString];
// Trim potential remaining white spaces
tag = [tag stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
// Closing current tag, so append result string
[resultString appendAttributedString:[self computeSuffixString]];
[self handleClosingTag:tag error:error];
}
else
{
// Check if autoclosing markup or not
autoclosingMarkup = [tag rangeOfString:@"/" options:NSBackwardsSearch].location == (tag.length - 1);
// If autoclosing markup, trim last /
if (autoclosingMarkup)
tag = [tag substringToIndex:tag.length - 1];
// Trim potential remaining white spaces
tag = [tag stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
// Split tag / attributes
tag = [self extractTagAndPushAttributesFromTag:tag];
// Handle autoclosing markup
if (autoclosingMarkup)
{
// Opening tag
[self handleOpeningTag:tag error:error];
// Append an extracted empty string
[resultString appendAttributedString:[self computePrefixString]];
[resultString appendAttributedString:[self computeFinalExtractedString:@""]];
[resultString appendAttributedString:[self computeSuffixString]];
// Close tag
[self handleClosingTag:tag error:error];
}
else
{
// Opening tag
[self handleOpeningTag:tag error:error];
[resultString appendAttributedString:[self computePrefixString]];
}
}
}
}
[resultString endEditing];
if (_configurationsStack.count != 0)
{
LOG_IF_DEBUG(GONMarkupParserLogLevelUnbalancedTags, @"Parsing completed, but stack isn't empty, some closing tags seems missing :\nStack :%@\n", _configurationsStack);
[self generateError:error tag:nil];
}
// Flush unuseful data
_markupsStack = nil;
_configurationsStack = nil;
_currentContext = nil;
_markupAttributesStack = nil;
return resultString;
}
#pragma mark - Utils
- (NSString *)extractTagAndPushAttributesFromTag:(NSString *)tag
{
// Check for attributes
NSRange range = [tag rangeOfString:@" "];
NSDictionary *attributes = nil;
NSString *extractedTag;
if (range.location == NSNotFound)
{
// No attributed to extract, and tag is full string
extractedTag = tag;
}
else
{
// There may be some attributes, so extract tag
extractedTag = [tag substringToIndex:range.location];
attributes = [self extractAttributesFromString:[tag substringFromIndex:range.location]];
}
// Check if some attributes were found
[_markupAttributesStack addObject:(attributes.count ? attributes : [NSNull null])];
return [extractedTag lowercaseString];
}
- (NSDictionary *)attributesForCurrentTag
{
id attributes = [_markupAttributesStack lastObject];
if (attributes == [NSNull null])
return nil;
return attributes;
}
- (NSDictionary *)extractAttributesFromString:(NSString *)string
{
NSMutableDictionary *dicAttributes = [[NSMutableDictionary alloc] init];
// Parse string
NSArray *results = [_attributesRegex matchesInString:string
options:0
range:NSMakeRange(0, string.length)];
// Browse chunks
NSString *matchedString;
NSRange range;
NSString *attributeKey;
NSMutableString *attributeValue;
for (NSTextCheckingResult *result in results)
{
// Extract matched string
matchedString = [string substringWithRange:result.range];
// Look for character to split string
range = [matchedString rangeOfString:@"="];
// Extract key
attributeKey = [[matchedString substringToIndex:range.location] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
// Extract value
attributeValue = [[[matchedString substringFromIndex:range.location + 1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy];
// Clean value, by trimming enclosing quotes / double quotes, cleaning potential " entities
[attributeValue replaceCharactersInRange:NSMakeRange(attributeValue.length - 1, 1) withString:@""];
[attributeValue replaceCharactersInRange:NSMakeRange(0, 1) withString:@""];
[attributeValue replaceOccurrencesOfString:@""" withString:@"\"" options:0 range:NSMakeRange(0, attributeValue.length)];
// Clean pontential html entities in string
[dicAttributes setObject:attributeValue
forKey:attributeKey];
}
return dicAttributes;
}
#pragma mark - Tag content managements
- (NSAttributedString *)computePrefixString
{
GONMarkup *currentMarker = [_markupsStack lastObject];
if (currentMarker && ![currentMarker isKindOfClass:[NSNull class]])
{
return [[NSAttributedString alloc] initWithString:[currentMarker prefixStringForContext:_currentContext attributes:[self attributesForCurrentTag]]
attributes:[self currentConfiguration]];
}
return [[NSAttributedString alloc] initWithString:@"" attributes:[self currentConfiguration]];
}
- (NSAttributedString *)computeSuffixString
{
GONMarkup *currentMarker = [_markupsStack lastObject];
if (currentMarker && ![currentMarker isKindOfClass:[NSNull class]])
{
return [[NSAttributedString alloc] initWithString:[currentMarker suffixStringForContext:_currentContext
attributes:[self attributesForCurrentTag]]
attributes:[self currentConfiguration]];
}
return [[NSAttributedString alloc] initWithString:@"" attributes:[self currentConfiguration]];
}
- (NSAttributedString *)computeFinalExtractedString:(NSString *)inputString
{
GONMarkup *currentMarker = [_markupsStack lastObject];
if (currentMarker && ![currentMarker isKindOfClass:[NSNull class]])
{
return [[NSAttributedString alloc] initWithString:[currentMarker updatedContentString:inputString
context:_currentContext
attributes:[self attributesForCurrentTag]]
attributes:[self currentConfiguration]];
}
return [[NSAttributedString alloc] initWithString:inputString attributes:[self currentConfiguration]];
}
#pragma mark - Tag managements
- (void)handleClosingTag:(NSString *)tag error:(NSError **)error
{
// Look for full style closing tag, @"//"
// Checking only for one /, because first one was trimmed
if ([tag rangeOfString:@"/"].location == 0)
{
if (!_configurationsStack.count)
{
LOG_IF_DEBUG(GONMarkupParserLogLevelUnbalancedTags, @"Trying to close all tags, but stack is empty");
[self generateError:error tag:tag];
}
else
{
GONMarkup *markup;
NSMutableDictionary *currentTagConfiguration;
for (NSInteger i=_markupsStack.count - 1; i>=0; i--)
{
markup = [_markupsStack objectAtIndex:i];
currentTagConfiguration = [_configurationsStack objectAtIndex:i];
[markup closingMarkupFound:tag
configuration:currentTagConfiguration
context:_currentContext
attributes:([_markupAttributesStack objectAtIndex:i] == [NSNull null] ? nil : [_markupAttributesStack objectAtIndex:i])];
}
[_configurationsStack removeAllObjects];
[_markupsStack removeAllObjects];
[_currentContext removeAllObjects];
[_markupAttributesStack removeAllObjects];
LOG_IF_DEBUG(GONMarkupParserLogLevelWorkflow, @"Closing all tags\nStack : %@\n", _configurationsStack);
}
}
else
{
// Check if available tags
if (!_configurationsStack.count)
{
// Closing a tag, but tags stack is empty
LOG_IF_DEBUG(GONMarkupParserLogLevelUnbalancedTags, @"Trying to close last tag, but stack is empty");
[self generateError:error tag:tag];
}
else
{
// Extract current markup
GONMarkup *markup = [_markupsStack lastObject];
// Present error when closing an unkwnow markup
if (markup && ![markup isKindOfClass:[NSNull class]])
{
// Check that closing that is matching opening one
if (tag.length)
{
if (![tag isEqualToString:markup.tag])
{
LOG_IF_DEBUG(GONMarkupParserLogLevelUnbalancedTags, @"Closing tag found <%@>, is not matching currently opened one <%@>", tag, markup.tag);
[self generateError:error tag:tag];
}
}
[markup closingMarkupFound:tag
configuration:[_configurationsStack lastObject]
context:_currentContext
attributes:[self attributesForCurrentTag]];
}
// Remove last tag objet
[_configurationsStack removeLastObject];
[_markupsStack removeLastObject];
[_markupAttributesStack removeLastObject];
LOG_IF_DEBUG(GONMarkupParserLogLevelWorkflow, @"Closing tag (%@)\nStack : %@\n", tag, _configurationsStack);
}
}
}
- (void)handleOpeningTag:(NSString *)tag error:(NSError **)error
{
// Prepare tag configuration
NSMutableDictionary *currentTagConfiguration;
if (!_configurationsStack.count)
currentTagConfiguration = [_defaultConfiguration mutableCopy];
else
currentTagConfiguration = [[_configurationsStack lastObject] mutableCopy];
// Retrieve markup associated to tag
GONMarkup *markup = [self markupForTag:tag];
// Ensure a markup was found
if (!markup)
{
LOG_IF_DEBUG(GONMarkupParserLogLevelUnknownTag, @"No markup found for tag <%@>\n", tag);
[self generateError:error
tag:tag];
[_markupsStack addObject:[NSNull null]];
}
else
{
[markup openingMarkupFound:tag
configuration:currentTagConfiguration
context:_currentContext
attributes:[self attributesForCurrentTag]];
[_markupsStack addObject:markup];
}
// Hold configuration
[_configurationsStack addObject:currentTagConfiguration];
LOG_IF_DEBUG(GONMarkupParserLogLevelWorkflow, @"Opening tag (%@)\nStack : %@\n", tag, _configurationsStack);
}
- (NSDictionary *)currentConfiguration
{
if (!_configurationsStack.count)
return [_defaultConfiguration copy];
return [[_configurationsStack lastObject] copy];
}
#pragma mark - Fonts management
- (void)registerFont:(UIFont *)font forKey:(NSString *)key
{
[_dicRegisteredFonts setObject:font
forKey:key];
}
- (UIFont *)fontForKey:(NSString *)key
{
return [_dicRegisteredFonts objectForKey:key];
}
- (void)unregisterFontForKey:(NSString *)key
{
[_dicRegisteredFonts removeObjectForKey:key];
}
#pragma mark - Error handling
- (void)generateError:(NSError **)error tag:(NSString *)tag
{
// Assert only is requested
if (_assertOnError)
NSAssert1(NO, @"An error was generated parsing following text, found at markup <%@>", tag);
if (error)
{
// Initialize user info
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:@"Input string is malformed. Ouput attributed string may not be displayed correctly"
forKey:NSLocalizedDescriptionKey];
// Add tag if avalaible
if (tag)
{
[userInfo setObject:tag
forKey:GONMarkupParser_incorrectClosingTag_KEY];
}
// Build error
*error = [NSError errorWithDomain:GONMarkupParser_ERROR_DOMAIN
code:GONMarkupParser_StringMalformed_ERROR_CODE
userInfo:userInfo];
}
}
#pragma mark - Getters
- (NSArray *)markups
{
return [[_dicCurrentMarkup allValues] copy];
}
- (NSDictionary *)registeredFonts
{
return [_dicRegisteredFonts copy];
}
@end
|