// // MMScanner.m // MMMarkdown // // Copyright (c) 2012 Matt Diephouse. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // #import "MMScanner.h" static NSString *__delimitersForCharacter(unichar character) { switch (character) { case '[': case ']': return @"[]"; case '(': case ')': return @"()"; case '<': case '>': return @"<>"; case '{': case '}': return @"{}"; default: [NSException raise:@"Invalid delimiter character" format:@"Character '%C' is not a valid delimiter", character]; return nil; } } @interface MMScanner () @property (assign, nonatomic) NSUInteger startLocation; @property (assign, nonatomic) NSRange currentRange; @property (assign, nonatomic, readonly) NSRange currentLineRange; @property (assign, nonatomic) NSUInteger rangeIndex; @property (strong, nonatomic, readonly) NSMutableArray *transactions; @end @implementation MMScanner #pragma mark - Public Methods + (id)scannerWithString:(NSString *)aString { return [[self.class alloc] initWithString:aString]; } - (id)initWithString:(NSString *)aString { NSArray *lineRanges = [self _lineRangesForString:aString]; return [self initWithString:aString lineRanges:lineRanges]; } + (id)scannerWithString:(NSString *)aString lineRanges:(NSArray *)theLineRanges { return [[self.class alloc] initWithString:aString lineRanges:theLineRanges]; } - (id)initWithString:(NSString *)aString lineRanges:(NSArray *)theLineRanges { NSParameterAssert(theLineRanges.count > 0); self = [super init]; if (self) { _string = aString; _lineRanges = theLineRanges; _rangeIndex = 0; _transactions = [NSMutableArray new]; self.startLocation = 0; self.currentRange = self.currentLineRange; } return self; } - (void)beginTransaction { NSDictionary *transaction = @{ @"rangeIndex": @(self.rangeIndex), @"location": @(self.location), @"startLocation": @(self.startLocation), }; [self.transactions addObject:transaction]; self.startLocation = self.location; } - (void)commitTransaction:(BOOL)shouldSave { if (!self.transactions.count) [NSException raise:@"Transaction underflow" format:@"Could not commit transaction because the stack is empty"]; NSDictionary *transaction = [self.transactions lastObject]; [self.transactions removeLastObject]; self.startLocation = [[transaction objectForKey:@"startLocation"] unsignedIntegerValue]; if (!shouldSave) { self.rangeIndex = [[transaction objectForKey:@"rangeIndex"] unsignedIntegerValue]; self.location = [[transaction objectForKey:@"location"] unsignedIntegerValue]; } } - (BOOL)atBeginningOfLine { return self.location == self.currentLineRange.location; } - (BOOL)atEndOfLine { return self.location == NSMaxRange(self.currentLineRange); } - (BOOL)atEndOfString { return self.atEndOfLine && self.rangeIndex == self.lineRanges.count - 1; } - (unichar)previousCharacter { if (self.atBeginningOfLine) return '\0'; return [self.string characterAtIndex:self.location - 1]; } - (unichar)nextCharacter { if (self.atEndOfLine) return '\n'; return [self.string characterAtIndex:self.location]; } - (NSString *)previousWord { return [self previousWordWithCharactersFromSet:NSCharacterSet.alphanumericCharacterSet]; } - (NSString *)nextWord { return [self nextWordWithCharactersFromSet:NSCharacterSet.alphanumericCharacterSet]; } - (NSString *)previousWordWithCharactersFromSet:(NSCharacterSet *)set { NSUInteger start = MAX(self.currentLineRange.location, self.startLocation); NSUInteger end = self.currentRange.location; NSRange range = NSMakeRange(start, end-start); NSRange result = [self.string rangeOfCharacterFromSet:set.invertedSet options:NSBackwardsSearch range:range]; if (result.location == NSNotFound) return [self.string substringWithRange:range]; NSUInteger wordLocation = NSMaxRange(result); NSRange wordRange = NSMakeRange(wordLocation, end-wordLocation); return [self.string substringWithRange:wordRange]; } - (NSString *)nextWordWithCharactersFromSet:(NSCharacterSet *)set { NSRange result = [self.string rangeOfCharacterFromSet:set.invertedSet options:0 range:self.currentRange]; if (result.location == NSNotFound) return [self.string substringWithRange:self.currentRange]; NSRange wordRange = self.currentRange; wordRange.length = result.location - wordRange.location; return [self.string substringWithRange:wordRange]; } - (void)advance { if (self.atEndOfLine) return; self.location += 1; } - (void)advanceToNextLine { // If at the last line, just go to the end of the line if (self.rangeIndex == self.lineRanges.count - 1) { self.location = NSMaxRange(self.currentLineRange); } // Otherwise actually go to the next line else { self.rangeIndex += 1; self.currentRange = self.currentLineRange; } } - (BOOL)matchString:(NSString *)string { if (self.currentRange.length < string.length) return NO; NSUInteger location = self.location; for (NSUInteger idx=0; idx 0) { if (self.atEndOfLine) { [self commitTransaction:NO]; return 0; } [self skipCharactersFromSet:boringChars]; unichar nextChar = self.nextCharacter; [self advance]; if (nextChar == openDelimiter) { nestingLevel++; } else if (nextChar == closeDelimeter) { nestingLevel--; } else if (nextChar == '\\') { // skip a second character after a backslash [self advance]; } } [self commitTransaction:YES]; return self.location - location; } - (NSUInteger)skipToEndOfLine { NSUInteger length = self.currentRange.length; self.location = NSMaxRange(self.currentRange); return length; } - (NSUInteger)skipToLastCharacterOfLine { NSUInteger length = self.currentRange.length - 1; self.location = NSMaxRange(self.currentRange) - 1; return length; } - (NSUInteger)skipWhitespace { return [self skipCharactersFromSet:NSCharacterSet.whitespaceCharacterSet]; } - (NSUInteger)skipWhitespaceAndNewlines { NSCharacterSet *whitespaceSet = NSCharacterSet.whitespaceCharacterSet; NSUInteger length = 0; while (!self.atEndOfString) { if (self.atEndOfLine) { [self advanceToNextLine]; length++; } else { NSUInteger spaces = [self skipCharactersFromSet:whitespaceSet]; if (spaces == 0) break; length += spaces; } } return length; } #pragma mark - Public Properties - (NSUInteger)location { return self.currentRange.location; } - (void)setLocation:(NSUInteger)location { // If the new location isn't a part of the current range, then find the range it belongs to. if (!NSLocationInRange(location, self.currentLineRange)) { __block NSUInteger index = 0; [self.lineRanges enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){ NSRange range = [obj rangeValue]; *stop = NSLocationInRange(location, range) || location == NSMaxRange(range); index = idx; }]; self.rangeIndex = index; } self.currentRange = NSMakeRange(location, NSMaxRange(self.currentLineRange)-location); } #pragma mark - Private Methods - (NSArray *)_lineRangesForString:(NSString *)aString { NSMutableArray *result = [NSMutableArray array]; NSUInteger location = 0; NSUInteger idx; for (idx=0; idx