kopia lustrzana
https://github.com/lint/TFDidThatSay
synced 2025-07-04 00:26:47 +00:00
500 wiersze
13 KiB
Objective-C
500 wiersze
13 KiB
Objective-C
//
|
|
// 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<string.length; idx++)
|
|
{
|
|
if ([string characterAtIndex:idx] != [self.string characterAtIndex:location+idx])
|
|
return NO;
|
|
}
|
|
|
|
self.location += string.length;
|
|
return YES;
|
|
}
|
|
|
|
- (NSUInteger)skipCharactersFromSet:(NSCharacterSet *)aSet
|
|
{
|
|
NSRange searchRange = self.currentRange;
|
|
NSRange range = [self.string rangeOfCharacterFromSet:[aSet invertedSet]
|
|
options:0
|
|
range:searchRange];
|
|
|
|
NSUInteger current = self.location;
|
|
|
|
if (range.location == NSNotFound)
|
|
{
|
|
self.currentRange = NSMakeRange(NSMaxRange(self.currentRange), 0);
|
|
}
|
|
else
|
|
{
|
|
self.currentRange = NSMakeRange(range.location, NSMaxRange(self.currentRange)-range.location);
|
|
}
|
|
|
|
return self.location - current;
|
|
}
|
|
|
|
- (NSUInteger)skipCharactersFromSet:(NSCharacterSet *)aSet max:(NSUInteger)maxToSkip
|
|
{
|
|
NSUInteger idx=0;
|
|
for (; idx<maxToSkip; idx++)
|
|
{
|
|
unichar character = [self nextCharacter];
|
|
if ([aSet characterIsMember:character])
|
|
[self advance];
|
|
else
|
|
break;
|
|
}
|
|
return idx;
|
|
}
|
|
|
|
- (NSUInteger)skipEmptyLines
|
|
{
|
|
NSUInteger skipped = 0;
|
|
|
|
while (![self atEndOfString])
|
|
{
|
|
[self beginTransaction];
|
|
[self skipWhitespace];
|
|
if (!self.atEndOfLine)
|
|
{
|
|
[self commitTransaction:NO];
|
|
break;
|
|
}
|
|
[self commitTransaction:YES];
|
|
[self advanceToNextLine];
|
|
skipped++;
|
|
}
|
|
|
|
return skipped;
|
|
}
|
|
|
|
- (NSUInteger)skipIndentationUpTo:(NSUInteger)maxSpacesToSkip
|
|
{
|
|
NSUInteger skipped = 0;
|
|
[self beginTransaction];
|
|
|
|
while (!self.atEndOfLine && skipped < maxSpacesToSkip)
|
|
{
|
|
unichar character = self.nextCharacter;
|
|
|
|
if (character == ' ')
|
|
skipped += 1;
|
|
else if (character == '\t')
|
|
{
|
|
skipped -= skipped % 4;
|
|
skipped += 4;
|
|
}
|
|
else
|
|
break;
|
|
|
|
[self advance];
|
|
}
|
|
|
|
[self commitTransaction:skipped <= maxSpacesToSkip];
|
|
return skipped;
|
|
}
|
|
|
|
- (NSUInteger)skipNestedBracketsWithDelimiter:(unichar)delimiter
|
|
{
|
|
NSString *delimiters = __delimitersForCharacter(delimiter);
|
|
|
|
if (delimiters == nil)
|
|
return 0;
|
|
|
|
unichar openDelimiter = [delimiters characterAtIndex:0];
|
|
unichar closeDelimeter = [delimiters characterAtIndex:1];
|
|
|
|
if ([self nextCharacter] != openDelimiter)
|
|
return 0;
|
|
|
|
[self beginTransaction];
|
|
NSUInteger location = self.location;
|
|
[self advance];
|
|
|
|
NSString *specialChars = [NSString stringWithFormat:@"%@\\", delimiters];
|
|
NSCharacterSet *boringChars = [[NSCharacterSet characterSetWithCharactersInString:specialChars] invertedSet];
|
|
NSUInteger nestingLevel = 1;
|
|
|
|
while (nestingLevel > 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<aString.length; idx++)
|
|
{
|
|
unichar character = [aString characterAtIndex:idx];
|
|
if (character == '\r' || character == '\n')
|
|
{
|
|
NSRange range = NSMakeRange(location, idx-location);
|
|
[result addObject:[NSValue valueWithRange:range]];
|
|
|
|
// If it's a carriage return, check for a line feed too
|
|
if (character == '\r')
|
|
{
|
|
if (idx + 1 < aString.length && [aString characterAtIndex:idx + 1] == '\n')
|
|
{
|
|
idx += 1;
|
|
}
|
|
}
|
|
|
|
location = idx + 1;
|
|
}
|
|
}
|
|
|
|
// Add the final line if the string doesn't end with a newline
|
|
if (location < aString.length)
|
|
{
|
|
NSRange range = NSMakeRange(location, aString.length-location);
|
|
[result addObject:[NSValue valueWithRange:range]];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSUInteger)_locationOfCharacter:(unichar)character inRange:(NSRange)range
|
|
{
|
|
NSString *characterString = [NSString stringWithCharacters:&character length:1];
|
|
NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:characterString];
|
|
NSRange result = [self.string rangeOfCharacterFromSet:characterSet options:0 range:range];
|
|
return result.location;
|
|
}
|
|
|
|
#pragma mark - Private Properties
|
|
|
|
- (NSRange)currentLineRange
|
|
{
|
|
return [self.lineRanges[self.rangeIndex] rangeValue];
|
|
}
|
|
|
|
@end
|