500 line
13KB

  1. //
  2. // MMScanner.m
  3. // MMMarkdown
  4. //
  5. // Copyright (c) 2012 Matt Diephouse.
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in
  15. // all copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. // THE SOFTWARE.
  24. //
  25. #import "MMScanner.h"
  26. static NSString *__delimitersForCharacter(unichar character)
  27. {
  28. switch (character)
  29. {
  30. case '[':
  31. case ']':
  32. return @"[]";
  33. case '(':
  34. case ')':
  35. return @"()";
  36. case '<':
  37. case '>':
  38. return @"<>";
  39. case '{':
  40. case '}':
  41. return @"{}";
  42. default:
  43. [NSException raise:@"Invalid delimiter character"
  44. format:@"Character '%C' is not a valid delimiter", character];
  45. return nil;
  46. }
  47. }
  48. @interface MMScanner ()
  49. @property (assign, nonatomic) NSUInteger startLocation;
  50. @property (assign, nonatomic) NSRange currentRange;
  51. @property (assign, nonatomic, readonly) NSRange currentLineRange;
  52. @property (assign, nonatomic) NSUInteger rangeIndex;
  53. @property (strong, nonatomic, readonly) NSMutableArray *transactions;
  54. @end
  55. @implementation MMScanner
  56. #pragma mark - Public Methods
  57. + (id)scannerWithString:(NSString *)aString
  58. {
  59. return [[self.class alloc] initWithString:aString];
  60. }
  61. - (id)initWithString:(NSString *)aString
  62. {
  63. NSArray *lineRanges = [self _lineRangesForString:aString];
  64. return [self initWithString:aString lineRanges:lineRanges];
  65. }
  66. + (id)scannerWithString:(NSString *)aString lineRanges:(NSArray *)theLineRanges
  67. {
  68. return [[self.class alloc] initWithString:aString lineRanges:theLineRanges];
  69. }
  70. - (id)initWithString:(NSString *)aString lineRanges:(NSArray *)theLineRanges
  71. {
  72. NSParameterAssert(theLineRanges.count > 0);
  73. self = [super init];
  74. if (self)
  75. {
  76. _string = aString;
  77. _lineRanges = theLineRanges;
  78. _rangeIndex = 0;
  79. _transactions = [NSMutableArray new];
  80. self.startLocation = 0;
  81. self.currentRange = self.currentLineRange;
  82. }
  83. return self;
  84. }
  85. - (void)beginTransaction
  86. {
  87. NSDictionary *transaction = @{
  88. @"rangeIndex": @(self.rangeIndex),
  89. @"location": @(self.location),
  90. @"startLocation": @(self.startLocation),
  91. };
  92. [self.transactions addObject:transaction];
  93. self.startLocation = self.location;
  94. }
  95. - (void)commitTransaction:(BOOL)shouldSave
  96. {
  97. if (!self.transactions.count)
  98. [NSException raise:@"Transaction underflow" format:@"Could not commit transaction because the stack is empty"];
  99. NSDictionary *transaction = [self.transactions lastObject];
  100. [self.transactions removeLastObject];
  101. self.startLocation = [[transaction objectForKey:@"startLocation"] unsignedIntegerValue];
  102. if (!shouldSave)
  103. {
  104. self.rangeIndex = [[transaction objectForKey:@"rangeIndex"] unsignedIntegerValue];
  105. self.location = [[transaction objectForKey:@"location"] unsignedIntegerValue];
  106. }
  107. }
  108. - (BOOL)atBeginningOfLine
  109. {
  110. return self.location == self.currentLineRange.location;
  111. }
  112. - (BOOL)atEndOfLine
  113. {
  114. return self.location == NSMaxRange(self.currentLineRange);
  115. }
  116. - (BOOL)atEndOfString
  117. {
  118. return self.atEndOfLine && self.rangeIndex == self.lineRanges.count - 1;
  119. }
  120. - (unichar)previousCharacter
  121. {
  122. if (self.atBeginningOfLine)
  123. return '\0';
  124. return [self.string characterAtIndex:self.location - 1];
  125. }
  126. - (unichar)nextCharacter
  127. {
  128. if (self.atEndOfLine)
  129. return '\n';
  130. return [self.string characterAtIndex:self.location];
  131. }
  132. - (NSString *)previousWord
  133. {
  134. return [self previousWordWithCharactersFromSet:NSCharacterSet.alphanumericCharacterSet];
  135. }
  136. - (NSString *)nextWord
  137. {
  138. return [self nextWordWithCharactersFromSet:NSCharacterSet.alphanumericCharacterSet];
  139. }
  140. - (NSString *)previousWordWithCharactersFromSet:(NSCharacterSet *)set
  141. {
  142. NSUInteger start = MAX(self.currentLineRange.location, self.startLocation);
  143. NSUInteger end = self.currentRange.location;
  144. NSRange range = NSMakeRange(start, end-start);
  145. NSRange result = [self.string rangeOfCharacterFromSet:set.invertedSet
  146. options:NSBackwardsSearch
  147. range:range];
  148. if (result.location == NSNotFound)
  149. return [self.string substringWithRange:range];
  150. NSUInteger wordLocation = NSMaxRange(result);
  151. NSRange wordRange = NSMakeRange(wordLocation, end-wordLocation);
  152. return [self.string substringWithRange:wordRange];
  153. }
  154. - (NSString *)nextWordWithCharactersFromSet:(NSCharacterSet *)set
  155. {
  156. NSRange result = [self.string rangeOfCharacterFromSet:set.invertedSet
  157. options:0
  158. range:self.currentRange];
  159. if (result.location == NSNotFound)
  160. return [self.string substringWithRange:self.currentRange];
  161. NSRange wordRange = self.currentRange;
  162. wordRange.length = result.location - wordRange.location;
  163. return [self.string substringWithRange:wordRange];
  164. }
  165. - (void)advance
  166. {
  167. if (self.atEndOfLine)
  168. return;
  169. self.location += 1;
  170. }
  171. - (void)advanceToNextLine
  172. {
  173. // If at the last line, just go to the end of the line
  174. if (self.rangeIndex == self.lineRanges.count - 1)
  175. {
  176. self.location = NSMaxRange(self.currentLineRange);
  177. }
  178. // Otherwise actually go to the next line
  179. else
  180. {
  181. self.rangeIndex += 1;
  182. self.currentRange = self.currentLineRange;
  183. }
  184. }
  185. - (BOOL)matchString:(NSString *)string
  186. {
  187. if (self.currentRange.length < string.length)
  188. return NO;
  189. NSUInteger location = self.location;
  190. for (NSUInteger idx=0; idx<string.length; idx++)
  191. {
  192. if ([string characterAtIndex:idx] != [self.string characterAtIndex:location+idx])
  193. return NO;
  194. }
  195. self.location += string.length;
  196. return YES;
  197. }
  198. - (NSUInteger)skipCharactersFromSet:(NSCharacterSet *)aSet
  199. {
  200. NSRange searchRange = self.currentRange;
  201. NSRange range = [self.string rangeOfCharacterFromSet:[aSet invertedSet]
  202. options:0
  203. range:searchRange];
  204. NSUInteger current = self.location;
  205. if (range.location == NSNotFound)
  206. {
  207. self.currentRange = NSMakeRange(NSMaxRange(self.currentRange), 0);
  208. }
  209. else
  210. {
  211. self.currentRange = NSMakeRange(range.location, NSMaxRange(self.currentRange)-range.location);
  212. }
  213. return self.location - current;
  214. }
  215. - (NSUInteger)skipCharactersFromSet:(NSCharacterSet *)aSet max:(NSUInteger)maxToSkip
  216. {
  217. NSUInteger idx=0;
  218. for (; idx<maxToSkip; idx++)
  219. {
  220. unichar character = [self nextCharacter];
  221. if ([aSet characterIsMember:character])
  222. [self advance];
  223. else
  224. break;
  225. }
  226. return idx;
  227. }
  228. - (NSUInteger)skipEmptyLines
  229. {
  230. NSUInteger skipped = 0;
  231. while (![self atEndOfString])
  232. {
  233. [self beginTransaction];
  234. [self skipWhitespace];
  235. if (!self.atEndOfLine)
  236. {
  237. [self commitTransaction:NO];
  238. break;
  239. }
  240. [self commitTransaction:YES];
  241. [self advanceToNextLine];
  242. skipped++;
  243. }
  244. return skipped;
  245. }
  246. - (NSUInteger)skipIndentationUpTo:(NSUInteger)maxSpacesToSkip
  247. {
  248. NSUInteger skipped = 0;
  249. [self beginTransaction];
  250. while (!self.atEndOfLine && skipped < maxSpacesToSkip)
  251. {
  252. unichar character = self.nextCharacter;
  253. if (character == ' ')
  254. skipped += 1;
  255. else if (character == '\t')
  256. {
  257. skipped -= skipped % 4;
  258. skipped += 4;
  259. }
  260. else
  261. break;
  262. [self advance];
  263. }
  264. [self commitTransaction:skipped <= maxSpacesToSkip];
  265. return skipped;
  266. }
  267. - (NSUInteger)skipNestedBracketsWithDelimiter:(unichar)delimiter
  268. {
  269. NSString *delimiters = __delimitersForCharacter(delimiter);
  270. if (delimiters == nil)
  271. return 0;
  272. unichar openDelimiter = [delimiters characterAtIndex:0];
  273. unichar closeDelimeter = [delimiters characterAtIndex:1];
  274. if ([self nextCharacter] != openDelimiter)
  275. return 0;
  276. [self beginTransaction];
  277. NSUInteger location = self.location;
  278. [self advance];
  279. NSString *specialChars = [NSString stringWithFormat:@"%@\\", delimiters];
  280. NSCharacterSet *boringChars = [[NSCharacterSet characterSetWithCharactersInString:specialChars] invertedSet];
  281. NSUInteger nestingLevel = 1;
  282. while (nestingLevel > 0)
  283. {
  284. if (self.atEndOfLine)
  285. {
  286. [self commitTransaction:NO];
  287. return 0;
  288. }
  289. [self skipCharactersFromSet:boringChars];
  290. unichar nextChar = self.nextCharacter;
  291. [self advance];
  292. if (nextChar == openDelimiter)
  293. {
  294. nestingLevel++;
  295. }
  296. else if (nextChar == closeDelimeter)
  297. {
  298. nestingLevel--;
  299. }
  300. else if (nextChar == '\\')
  301. {
  302. // skip a second character after a backslash
  303. [self advance];
  304. }
  305. }
  306. [self commitTransaction:YES];
  307. return self.location - location;
  308. }
  309. - (NSUInteger)skipToEndOfLine
  310. {
  311. NSUInteger length = self.currentRange.length;
  312. self.location = NSMaxRange(self.currentRange);
  313. return length;
  314. }
  315. - (NSUInteger)skipToLastCharacterOfLine
  316. {
  317. NSUInteger length = self.currentRange.length - 1;
  318. self.location = NSMaxRange(self.currentRange) - 1;
  319. return length;
  320. }
  321. - (NSUInteger)skipWhitespace
  322. {
  323. return [self skipCharactersFromSet:NSCharacterSet.whitespaceCharacterSet];
  324. }
  325. - (NSUInteger)skipWhitespaceAndNewlines
  326. {
  327. NSCharacterSet *whitespaceSet = NSCharacterSet.whitespaceCharacterSet;
  328. NSUInteger length = 0;
  329. while (!self.atEndOfString)
  330. {
  331. if (self.atEndOfLine)
  332. {
  333. [self advanceToNextLine];
  334. length++;
  335. }
  336. else
  337. {
  338. NSUInteger spaces = [self skipCharactersFromSet:whitespaceSet];
  339. if (spaces == 0)
  340. break;
  341. length += spaces;
  342. }
  343. }
  344. return length;
  345. }
  346. #pragma mark - Public Properties
  347. - (NSUInteger)location
  348. {
  349. return self.currentRange.location;
  350. }
  351. - (void)setLocation:(NSUInteger)location
  352. {
  353. // If the new location isn't a part of the current range, then find the range it belongs to.
  354. if (!NSLocationInRange(location, self.currentLineRange))
  355. {
  356. __block NSUInteger index = 0;
  357. [self.lineRanges enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){
  358. NSRange range = [obj rangeValue];
  359. *stop = NSLocationInRange(location, range) || location == NSMaxRange(range);
  360. index = idx;
  361. }];
  362. self.rangeIndex = index;
  363. }
  364. self.currentRange = NSMakeRange(location, NSMaxRange(self.currentLineRange)-location);
  365. }
  366. #pragma mark - Private Methods
  367. - (NSArray *)_lineRangesForString:(NSString *)aString
  368. {
  369. NSMutableArray *result = [NSMutableArray array];
  370. NSUInteger location = 0;
  371. NSUInteger idx;
  372. for (idx=0; idx<aString.length; idx++)
  373. {
  374. unichar character = [aString characterAtIndex:idx];
  375. if (character == '\r' || character == '\n')
  376. {
  377. NSRange range = NSMakeRange(location, idx-location);
  378. [result addObject:[NSValue valueWithRange:range]];
  379. // If it's a carriage return, check for a line feed too
  380. if (character == '\r')
  381. {
  382. if (idx + 1 < aString.length && [aString characterAtIndex:idx + 1] == '\n')
  383. {
  384. idx += 1;
  385. }
  386. }
  387. location = idx + 1;
  388. }
  389. }
  390. // Add the final line if the string doesn't end with a newline
  391. if (location < aString.length)
  392. {
  393. NSRange range = NSMakeRange(location, aString.length-location);
  394. [result addObject:[NSValue valueWithRange:range]];
  395. }
  396. return result;
  397. }
  398. - (NSUInteger)_locationOfCharacter:(unichar)character inRange:(NSRange)range
  399. {
  400. NSString *characterString = [NSString stringWithCharacters:&character length:1];
  401. NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:characterString];
  402. NSRange result = [self.string rangeOfCharacterFromSet:characterSet options:0 range:range];
  403. return result.location;
  404. }
  405. #pragma mark - Private Properties
  406. - (NSRange)currentLineRange
  407. {
  408. return [self.lineRanges[self.rangeIndex] rangeValue];
  409. }
  410. @end