PageRenderTime 50ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/STSTemplateEngine.m

https://bitbucket.org/sunrisetel.co.jp/sts-template-engine
Objective C | 1610 lines | 746 code | 106 blank | 758 comment | 165 complexity | 1b86bfc2ba3c91efbaa59244ae423dc0 MD5 | raw file
Possible License(s): GPL-2.0

Large files files are truncated, but you can click here to view the full file

  1. //
  2. // STSTemplateEngine.m
  3. // STS Template Engine ver 1.00
  4. //
  5. // A universal template engine with conditional template expansion support.
  6. //
  7. // Created by benjk on 6/28/05.
  8. // Copyright 2005 Sunrise Telephone Systems Ltd. All rights reserved.
  9. //
  10. // This software is released as open source under the terms of the General
  11. // Public License (GPL) version 2. A copy of the GPL license should have
  12. // been distributed along with this software. If you have not received a
  13. // copy of the GPL license, you can obtain it from Free Software Foundation
  14. // Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
  15. //
  16. // Permission is hereby granted to link this code against Apple's proprietary
  17. // Cocoa Framework, regardless of the limitations in the GPL license. The
  18. // Copyright notice and credits must be preserved at all times in every
  19. // redistributed copy and any derivative work of this software.
  20. //
  21. // THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY KIND,
  22. // WHETHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY IMPLIED
  23. // WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR ANY PARTICULAR PURPOSE. THE
  24. // ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THIS SOFTWARE LIES WITH
  25. // THE LICENSEE. FOR FURTHER INFORMATION PLEASE REFER TO THE GPL VERSION 2.
  26. //
  27. // For projects and software products for which the terms of the GPL license
  28. // are not suitable, alternative licensing can be obtained directly from
  29. // Sunrise Telephone Systems Ltd. at http://www.sunrise-tel.com
  30. //
  31. #import <LIFO.h>
  32. #import <STSStringOps.h>
  33. #import <STSNullError.h>
  34. #import "STSTemplateEngine.h"
  35. #define TODO {}; /* dummy statement for future sections */
  36. // ---------------------------------------------------------------------------
  37. // String literals
  38. // ---------------------------------------------------------------------------
  39. #define kEmptyString @""
  40. #define kLineFeed @"\n"
  41. // ---------------------------------------------------------------------------
  42. // Macro for use in if-clauses when testing NSRange variables
  43. // ---------------------------------------------------------------------------
  44. #define found(x) (x.location != NSNotFound)
  45. // ---------------------------------------------------------------------------
  46. // Macros for testing if a key is present in an NSDictionary
  47. // ---------------------------------------------------------------------------
  48. #define keyDefined(x,y) ([x objectForKey:y] != nil)
  49. #define keyNotDefined(x,y) ([x objectForKey:y] == nil)
  50. // ---------------------------------------------------------------------------
  51. // P r i v a t e F u n c t i o n s
  52. // ---------------------------------------------------------------------------
  53. // ---------------------------------------------------------------------------
  54. // Private Function: classExists(className)
  55. // ---------------------------------------------------------------------------
  56. //
  57. // Returns YES if class className exists, otherwise NO.
  58. BOOL classExists (NSString *className) {
  59. Class classPtr = NSClassFromString(className);
  60. return (classPtr != nil);
  61. } // end function
  62. // ---------------------------------------------------------------------------
  63. // P r i v a t e C a t e g o r i e s
  64. // ---------------------------------------------------------------------------
  65. @interface NSFileManager (STSTemplateEnginePrivateCategory1)
  66. // ---------------------------------------------------------------------------
  67. // Private Method: isRegularFileAtPath:
  68. // ---------------------------------------------------------------------------
  69. //
  70. // Returns YES if the file specified in path is a regular file, or NO if it is
  71. // not. This method traverses symbolic links.
  72. - (BOOL)isRegularFileAtPath:(NSString *)path;
  73. @end
  74. @implementation NSFileManager (STSTemplateEnginePrivateCategory1);
  75. // ---------------------------------------------------------------------------
  76. // Instance Method: isRegularFileAtPath:
  77. // ---------------------------------------------------------------------------
  78. //
  79. // description:
  80. // Returns YES if the file specified in path is a regular file, or NO if it
  81. // is not. If path specifies a symbolic link, this method traverses the link
  82. // and returns YES or NO based on the existence of the file at the link
  83. // destination. If path begins with a tilde, it must first be expanded with
  84. // stringByExpandingTildeInPath, or this method will return NO.
  85. //
  86. // pre-conditions:
  87. // receiver must be an object of class NSFileManager.
  88. // path must be a valid POSIX pathname.
  89. //
  90. // post-conditions:
  91. // return value is of type BOOL and contains YES if the file at path is a
  92. // regular file or if it is a symbolic link pointing to a regular file.
  93. // Otherwise, NO is returned.
  94. - (BOOL)isRegularFileAtPath:(NSString *)path
  95. {
  96. return ([[[self fileAttributesAtPath:path traverseLink:YES] fileType]
  97. isEqualToString:@"NSFileTypeRegular"]);
  98. } // end method
  99. @end // private category
  100. @interface NSString (STSTemplateEnginePrivateCategory2)
  101. // ===========================================================================
  102. // NOTE regarding the use of string literals with non 7-bit ASCII characters
  103. // ---------------------------------------------------------------------------
  104. // The Cocoa runtime system always interprets string literals as if they
  105. // were encoded in the system's default string encoding which is dependent on
  106. // the current locale, regardless of their true encoding. Thus, if a source
  107. // file is saved in a different encoding, all its string literals will contain
  108. // data encoded with a different encoding than the encoding for the current
  109. // locale but the runtime system will nevertheless interpret the data as
  110. // having been encoded in the encoding for the current locale. Xcode does not
  111. // adjust for this and as a result, characters which are not 7-bit ASCII
  112. // characters will be garbled.
  113. // The default start and end tags of the template engine are non 7-bit
  114. // ASCII characters. If they are passed as string literals and the source code
  115. // is not in the same encoding as the current locale at the time the source
  116. // code is compiled, the template engine will not work properly. Therefore
  117. // the tags have to be embedded in an encoding-safe manner. This is what the
  118. // following methods defaultStartTag and defaultEndTag are for.
  119. // ===========================================================================
  120. // ---------------------------------------------------------------------------
  121. // Private Method: defaultStartTag
  122. // ---------------------------------------------------------------------------
  123. //
  124. // Returns a new NSString initialised with the template engine's default start
  125. // tag (the percent sign followed by the opening chevrons, "%«"). This method
  126. // is source file encoding-safe.
  127. + (NSString *)defaultStartTag;
  128. // ---------------------------------------------------------------------------
  129. // Private Method: defaultEndTag
  130. // ---------------------------------------------------------------------------
  131. //
  132. // Returns a new NSString initialised with the template engine's default end
  133. // tag (the closing chevrons, "»"). This method is source file encoding-safe.
  134. + (NSString *)defaultEndTag;
  135. // ---------------------------------------------------------------------------
  136. // Private Method: stringByExpandingPlaceholdersWithStartTag:andEndTag:
  137. // usingDictionary:errorsReturned:lineNumber:
  138. // ---------------------------------------------------------------------------
  139. //
  140. // This method is invoked by the public methods below in order to expand
  141. // individual lines in a template string or template file. It returns the
  142. // receiver with tagged placeholders expanded. Placeholders are recognised by
  143. // start and end tags passed and they are expanded by using key/value pairs in
  144. // the dictionary passed. An errorLog is returned to indicate whether the
  145. // expansion has been successful.
  146. // If a placeholder cannot be expanded, a partially expanded string is
  147. // returned with one or more error messages inserted or appended and an
  148. // error description of class TEError is added to an NSArray returned in
  149. // errorLog. lineNumber is used to set the error description's line number.
  150. - (NSString *)stringByExpandingPlaceholdersWithStartTag:(NSString *)startTag
  151. andEndTag:(NSString *)endTag
  152. usingDictionary:(NSDictionary *)dictionary
  153. errorsReturned:(NSArray **)errorLog
  154. lineNumber:(unsigned)lineNumber;
  155. @end
  156. @implementation NSString (STSTemplateEnginePrivateCategory2);
  157. // ---------------------------------------------------------------------------
  158. // Class Method: defaultStartTag
  159. // ---------------------------------------------------------------------------
  160. //
  161. // Returns a new NSString initialised with the template engine's default start
  162. // tag (the percent sign followed by the opening chevrons, "%«"). This method
  163. // is source file encoding-safe.
  164. + (NSString *)defaultStartTag
  165. {
  166. NSData *data;
  167. // avoid the use of string literals to be source file encoding-safe
  168. // use MacOS Roman hex codes for percent and opening chevrons "%«"
  169. char octets[2] = { 0x25, 0xc7 };
  170. data = [NSData dataWithBytes:&octets length:2];
  171. return [[[NSString alloc] initWithData:data encoding:NSMacOSRomanStringEncoding] autorelease];
  172. } // end method
  173. // ---------------------------------------------------------------------------
  174. // Class Method: defaultEndTag
  175. // ---------------------------------------------------------------------------
  176. //
  177. // Returns a new NSString initialised with the template engine's default end
  178. // tag (the closing chevrons, "»"). This method is source file encoding-safe.
  179. + (NSString *)defaultEndTag
  180. {
  181. NSData *data;
  182. // avoid the use of string literals to be source file encoding-safe
  183. // use MacOS Roman hex code for closing chevrons "»"
  184. char octet = 0xc8;
  185. data = [NSData dataWithBytes:&octet length:1];
  186. return [[[NSString alloc] initWithData:data encoding:NSMacOSRomanStringEncoding] autorelease];
  187. } // end method
  188. // ---------------------------------------------------------------------------
  189. // Instance Method: stringByExpandingPlaceholdersWithStartTag:andEndTag:
  190. // usingDictionary:errorsReturned:lineNumber:
  191. // ---------------------------------------------------------------------------
  192. //
  193. // description:
  194. // Returns the receiver with tagged placeholders expanded. Placeholders are
  195. // recognised by start and end tags passed and they are expanded by using
  196. // key/value pairs in the dictionary passed. A status code passed by
  197. // reference is set to indicate whether the expansion has been successful.
  198. // If a placeholder cannot be expanded, a partially expanded string is
  199. // returned with one or more error messages inserted or appended and an
  200. // error description of class TEError is added to an NSArray returned in
  201. // errorLog. lineNumber is used to set the error description's line number.
  202. //
  203. // pre-conditions:
  204. // startTag and endTag must not be empty strings and must not be nil.
  205. // dictionary contains keys to be replaced by their respective values.
  206. //
  207. // post-conditions:
  208. // Return value contains the receiver with all placeholders expanded for
  209. // which the dictionary contains keys. If there are no placeholders in the
  210. // receiver, the receiver will be returned unchanged.
  211. // Any placeholders for which the dictionary does not contain keys will
  212. // remain in their tagged placeholder form and have an error message
  213. // appended to them in the returned string. If a placeholder without
  214. // a closing tag is found, the offending placeholder will remain in its
  215. // incomplete start tag only form and have an error message appended to it.
  216. // Any text following the offending start tag only placeholder will not be
  217. // processed. An NSArray with error descriptions of class TEError will be
  218. // passed in errorLog to the caller.
  219. //
  220. // error-conditions:
  221. // If start tag is empty or nil, an exception StartTagEmptyOrNil will be
  222. // raised. If end tag is empty or nil, an exception EndTagEmptyOrNil will be
  223. // raised. It is the responsibility of the caller to catch the exception.
  224. - (NSString *)stringByExpandingPlaceholdersWithStartTag:(NSString *)startTag
  225. andEndTag:(NSString *)endTag
  226. usingDictionary:(NSDictionary *)dictionary
  227. errorsReturned:(NSArray **)errorLog
  228. lineNumber:(unsigned)lineNumber
  229. {
  230. NSMutableString *remainder = [NSMutableString stringWithCapacity:[self length]];
  231. NSMutableString *result = [NSMutableString stringWithCapacity:[self length]];
  232. NSMutableString *placeholder = [NSMutableString stringWithCapacity:20];
  233. NSMutableString *value = [NSMutableString stringWithCapacity:20];
  234. NSMutableArray *_errorLog = [NSMutableArray arrayWithCapacity:5];
  235. #define errorsHaveOcurred ([_errorLog count] > 0)
  236. NSException* exception;
  237. NSRange tag, range;
  238. TEError *error;
  239. // check if start tag is nil or empty
  240. if ((startTag == nil) || ([startTag length] == 0)) {
  241. // this is a fatal error -- bail out by raising an exception
  242. [NSException exceptionWithName:@"TEStartTagEmptyOrNil"
  243. reason:@"startTag is empty or nil" userInfo:nil];
  244. [exception raise];
  245. } // end if
  246. // check if end tag is nil or empty
  247. if ((endTag == nil) || ([endTag length] == 0)) {
  248. // this is a fatal error -- bail out by raising an exception
  249. [NSException exceptionWithName:@"TEEndTagEmptyOrNil"
  250. reason:@"endTag is empty or nil" userInfo:nil];
  251. [exception raise];
  252. } // end if
  253. // initialise the source string
  254. [remainder setString:self];
  255. // look for the initial start tag
  256. tag = [remainder rangeOfString:startTag];
  257. // if we find a start tag ...
  258. if found(tag) {
  259. // continue for as long as we find start tags
  260. while found(tag) {
  261. // append substring before start tag to the result string
  262. [result appendString:[remainder substringToIndex:tag.location]];
  263. // remove preceeding text and start tag from the remainder
  264. range.location = 0; range.length = tag.location+tag.length;
  265. [remainder deleteCharactersInRange:range];
  266. // look for the end tag
  267. tag = [remainder rangeOfString:endTag];
  268. // if we did find the end tag ...
  269. if found(tag) {
  270. // extract the placeholder
  271. [placeholder setString:[remainder substringToIndex:tag.location]];
  272. // use placeholder as key for dictionary lookup
  273. value = [dictionary objectForKey:placeholder];
  274. // if the lookup returned nil (key not found)
  275. if (value == nil) {
  276. // append the tagged placeholder and an error message to the result string
  277. [result appendFormat:@"%@%@%@ *** ERROR: undefined key *** ", startTag, placeholder, endTag];
  278. // this is an error - create a new error description
  279. error = [TEError error:TE_UNDEFINED_PLACEHOLDER_FOUND_ERROR
  280. inLine:lineNumber atToken:TE_PLACEHOLDER];
  281. [error setLiteral:placeholder];
  282. // and add this error to the error log
  283. [_errorLog addObject:error];
  284. // log this error to the console
  285. [error logErrorMessageForTemplate:kEmptyString];
  286. }
  287. // if the lookup returns a value for the key ...
  288. else {
  289. // append the key's value to the result string
  290. [result appendString:value];
  291. } // end if
  292. // remove placeholder and end tag from the remainder
  293. range.location = 0; range.length = tag.location+tag.length;
  294. [remainder deleteCharactersInRange:range];
  295. } // end if
  296. // if we don't find any end tag ...
  297. else {
  298. // append the start tag and an error message to the result string
  299. [result appendFormat:@"%@ *** ERROR: end tag missing *** ", startTag];
  300. // remove all remaining text from the source string to force exit of while loop
  301. [remainder setString:kEmptyString];
  302. // this is an error - create a new error description
  303. error = [TEError error:TE_EXPECTED_ENDTAG_BUT_FOUND_TOKEN_ERROR
  304. inLine:lineNumber atToken:TE_EOL];
  305. // and add this error to the error log
  306. [_errorLog addObject:error];
  307. // log this error to the console
  308. [error logErrorMessageForTemplate:kEmptyString];
  309. } // end if
  310. // look for follow-on start tag to prepare for another parsing cycle
  311. tag = [remainder rangeOfString:startTag];
  312. } // end while
  313. // if there are still characters in the remainder ...
  314. if ([remainder length] > 0) {
  315. // append the remaining characters to the result
  316. [result appendString:remainder];
  317. } // end if
  318. }
  319. // if we don't find a start tag ...
  320. else {
  321. // then there is nothing to expand and we return the original as is
  322. result = remainder;
  323. } // end if
  324. // if there were any errors ...
  325. if (errorsHaveOcurred) {
  326. // pass the error log back to the caller
  327. *errorLog = _errorLog;
  328. // get rid of the following line after testing
  329. NSLog(@"errors have ocurred while expanding placeholders in string");
  330. }
  331. // if there were no errors ...
  332. else {
  333. // pass nil in the errorLog back to the caller
  334. *errorLog = nil;
  335. } // end if
  336. // return the result string
  337. return result;
  338. #undef errorsHaveOcurred
  339. } // end method
  340. @end // private category
  341. // ---------------------------------------------------------------------------
  342. // P r i v a t e C l a s s e s
  343. // ---------------------------------------------------------------------------
  344. @interface TEFlags : NSObject {
  345. // instance variable declaration
  346. @public unsigned consumed, expand, condex;
  347. } // end var
  348. //
  349. // public method: return new flags, allocated, initialised and autoreleased.
  350. // initial values: consumed is false, expand is true, condex is false.
  351. + (TEFlags *)newFlags;
  352. @end
  353. @implementation TEFlags
  354. // private method: initialise instance
  355. - (id)init {
  356. self = [super init];
  357. return self;
  358. } // end method
  359. // private method: deallocate instance
  360. - (void)dealloc {
  361. [super dealloc];
  362. } // end method
  363. // public method: return new flags, allocated, initialised and autoreleased.
  364. // initial values: consumed is false, expand is true, condex is false.
  365. + (TEFlags *)newFlags {
  366. TEFlags *thisInstance = [[[TEFlags alloc] init] autorelease];
  367. // initialise flags
  368. thisInstance->consumed = false;
  369. thisInstance->expand = true;
  370. thisInstance->condex = false;
  371. return thisInstance;
  372. } // end method
  373. @end // private class
  374. // ---------------------------------------------------------------------------
  375. // P u b l i c C a t e g o r y I m p l e m e n t a t i o n
  376. // ---------------------------------------------------------------------------
  377. @implementation NSString (STSTemplateEngine);
  378. // ---------------------------------------------------------------------------
  379. // Class Method: stringByExpandingTemplate:usingDictionary:errorsReturned:
  380. // ---------------------------------------------------------------------------
  381. //
  382. // description:
  383. // Invokes method stringByExpandingTemplate:withStartTag:andEndTag:
  384. // usingDictionary:errorsReturned: with the template engine's default tags:
  385. // startTag "%«" and endTag "»". This method is source file encoding-safe.
  386. + (id)stringByExpandingTemplate:(NSString *)templateString
  387. usingDictionary:(NSDictionary *)dictionary
  388. errorsReturned:(NSArray **)errorLog
  389. {
  390. return [NSString stringByExpandingTemplate:templateString
  391. withStartTag:[NSString defaultStartTag]
  392. andEndTag:[NSString defaultEndTag]
  393. usingDictionary:dictionary
  394. errorsReturned:errorLog];
  395. } // end method
  396. // ---------------------------------------------------------------------------
  397. // Class Method: stringByExpandingTemplate:withStartTag:andEndTag:
  398. // usingDictionary:errorsReturned:
  399. // ---------------------------------------------------------------------------
  400. //
  401. // description:
  402. // Returns a new NSString made by expanding templateString. Lines starting
  403. // with a % character are interpreted as comments or directives for the
  404. // template engine. Directives are %IF, %IFNOT, %IFEQ, %IFNEQ, %IFDEF,
  405. // %IFNDEF, %ELSIF, %ELSIFNOT, %ELSIFEQ, %ELSIFNEQ, %ELSIFDEF, %ELSIFNDEF,
  406. // %ELSE, %ENDIF, %DEFINE, %UNDEF, %LOG, %ECHO and %DEBUG.
  407. // Any line starting with a % character that is not part of a valid directive
  408. // nor part of a start tag is treated as a comment. Comment lines are not
  409. // copied to the result returned.
  410. // The %IF, %IFNOT, %IFEQ, %IFNEQ, %IFDEF, %IFNDEF, %ELSIF, %ELSEIFNOT,
  411. // %ELSIFEQ, %ELSIFNEQ, %ELSIFDEF, %ELSIFNDEF, %ELSE and %ENDIF directives
  412. // are for conditional template expansion. Any %IF, %IFNOT, %IFEQ, %IFNEQ,
  413. // %IFDEF or %IFNDEF directive opens a new if-block and a new if-branch
  414. // within the new if-block. Any %ELSIF, %ELSIFNOT, %ELSIFEQ, %ELSIFNEQ,
  415. // %ELSIFDEF or %ELSEIFNDEF directive opens a new else-if branch in the
  416. // current if-block. An %ELSE directive opens an else-branch in the current
  417. // if-block. An %ENDIF directive closes the current if-block.
  418. // An identifier following %IF is interpreted as a key which is looked
  419. // up in the dictionary. If the key's value represents logical true, the
  420. // subsequent lines are expanded until an elsif-, else- or endif-directive
  421. // is found or another if-block is opened.
  422. // An identifier following %IFNOT is interpreted as a key which is
  423. // looked up in the dictionary. If the key's value does not represent logical
  424. // true, the subsequent lines are expanded until an elsif-, else- or endif-
  425. // directive is found or another if-block is opened.
  426. // A key's value represents logical true if its all-lowercase
  427. // representation is "1", "yes" or "true".
  428. // An identifier following %IFEQ is interpreted as a key which is looked
  429. // up in the dictionary and its value is then compared to the operand that
  430. // follows the key. If the key's value and the operand match, the subsequent
  431. // lines are expanded until an elsif-, else- or endif-directive is found or
  432. // another if block is opened.
  433. // An identifier following %IFNEQ is interpreted as a key which is
  434. // looked up in the dictionary and its value is then compared to the operand
  435. // that follows the key. If the key's value and the operand do not match,
  436. // the subsequent lines are expanded until an elsif-, else- or endif-
  437. // directive is found or another if block is opened.
  438. // An identifier following %IFDEF is interpreted as a key which is
  439. // looked up in the dictionary. If the key is found in the dictionary, the
  440. // subsequent lines are expanded until an elsif-, else- or endif-
  441. // directive is found or another if-block is opened.
  442. // An identifier following %IFNDEF is interpreted as a key which is
  443. // looked up in the dictionary. If the key is not found in the dictionary,
  444. // the subsequent lines are expanded until an elsif-, else- or endif-
  445. // directive is found or another if-block is opened.
  446. // An %ELSEIF, %ELSIFNOT, %ELSIFEQ, %ELSIFNEQ, %ELSIFDEF or %ELSIFNDEF
  447. // directive opens an else-if branch in the current if block. An else-if
  448. // directive will only be evaluated if no prior if- or else-if branch was
  449. // expanded. The expression following such an else-if directive is evaluated
  450. // in the same way an expression following the corresponding if-directive is
  451. // evaluated.
  452. // An %ELSE directive opens an else branch in the current if-block. The
  453. // lines following an else branch will be expanded if no prior if- or else-if
  454. // branch was expanded. Lines are expanded until an %ENDIF directive is found
  455. // or another if-block is opened.
  456. // Any section outside any an if-block is expanded unconditionally,
  457. // excluding comment lines which are always ignored. If-blocks may be nested.
  458. // A %DEFINE directive followed by a key name causes that key to be added
  459. // to the dictionary. If any text follows the key name, that text is stored
  460. // as the key's value, otherwise the key's value will be an empty string. If
  461. // the key already exists in the dictionary then it's value will be replaced.
  462. // An %UNDEF directive followed by a key name causes that key to be
  463. // removed from the dictionary.
  464. // A %LOG directive followed by a key will cause that key and its value
  465. // to be logged to the console, which may be used for troubleshooting.
  466. // The %ECHO and %DEBUG directives are ignored as they have not been
  467. // implemented yet.
  468. // Any lines to be expanded which contain tagged placeholders are copied
  469. // to the result returned with the tagged placeholders expanded. Any lines
  470. // to be expanded which do not contain any placeholders are copied verbatim
  471. // to the result returned. Placeholders are recognised by start and end tags
  472. // passed in startTag and endTag and they are expanded by using key/value
  473. // pairs in dictionary. Placeholder names starting with an underscore "_"
  474. // character are reserved for automatic placeholder variables.
  475. // Automatic placeholder variables are automatically entered into the
  476. // dictionary by the template engine. Currently defined automatic placeholder
  477. // variables are: _timestamp, _uniqueID and _hostname.
  478. // The value of _timestamp is a datetime string with the system's current
  479. // date and time value formatted to follow the international string
  480. // representation format YYYY-MM-DD HH:MM:SS ±HHMM at the time the method is
  481. // invoked.
  482. // The value of _uniqueID is a globally unique ID string as generated by
  483. // method globallyUniqueString of class NSProcessInfo. For each invocation of
  484. // stringByExpandingTemplate: withStartTag:andEndTag:usingDictionary:
  485. // errorsReturned: a new value for _uniqueID is generated.
  486. // The value of _hostname is the system's host name at the time the
  487. // method is invoked.
  488. // On MacOS X 10.4 "Tiger" (and later) locale information is available
  489. // through automatic placeholder variables _userCountryCode, _userLanguage,
  490. // _systemCountryCode and _systemLanguage.
  491. // For every placeholder that cannot be expanded, a partially expanded
  492. // line is copied to the result returned with one or more error messages
  493. // inserted or appended.
  494. // An NSArray containing descriptions of any errors that may have ocurred
  495. // during expansion is passed back in errorLog to the caller. If expansion
  496. // completed withough any errors, then errorLog is set to nil.
  497. //
  498. // pre-conditions:
  499. // templateString is an NSString containing the template to be expanded.
  500. // startTag and endTag must not be empty strings and must not be nil.
  501. // dictionary contains keys to be replaced by their respective values.
  502. // errorLog is an NSArray passed by reference.
  503. //
  504. // post-conditions:
  505. // Return value contains a new NSString made by expanding templateString
  506. // with lines outside of %IF blocks expanded unconditionally and lines
  507. // inside of %IF blocks expanded conditionally. If any errors are
  508. // encountered during template expansion, errorLog will be set to an NSArray
  509. // containing error descriptions of class TEError for each error or warning.
  510. // If there were neither errors nor warnings during template expansion,
  511. // errorLog is set to nil.
  512. // Template expansion errors are treated gracefully. Various error
  513. // recovery strategies ensure that expansion can continue even in the event
  514. // of errors encountered in the template and error descriptions are added to
  515. // errorLog.
  516. // If an if-directive is not followed by an expression, the entire
  517. // if-block opened by that directive will be ignored, that is all lines will
  518. // be ignored until a matching endif-directive is found. An error description
  519. // is added to errorLog and an error message is written to the console log.
  520. // If an else-if directive is not followed by an expression, the else-if
  521. // branch opened by that directive will be ignored, that is all lines will be
  522. // ignored until an else-if-, else- or endif-directive is found or another
  523. // if-block is opened.
  524. // If any else-if-, else- or endif-directive appears without a prior
  525. // if-directive, then the line with that directive will be ignored and
  526. // expansion continues accordingly. An error description is added to
  527. // errorLog and an error message is written to the console log.
  528. // If the end of the template file is reached before an if-block was
  529. // closed by an endif-directive, the block is deemed to have been closed
  530. // implicitly. An error description is added to errorLog and an error
  531. // message is written to the console log.
  532. // Any placeholders for which the dictionary does not contain keys will
  533. // remain in their tagged placeholder form and have an error message
  534. // appended to them in the corresponding expanded line. Expansion continues
  535. // accordingly. An error description is added to errorLog and logged to the
  536. // console.
  537. // If a placeholder without a closing tag is found, the offending
  538. // placeholder will remain in its incomplete start tag only form, have an
  539. // error message appended to it and the remainder of the line is not
  540. // processed. Expansion then continues in the line that follows. An error
  541. // description is added to errorLog and logged to the console.
  542. // When template expansion is completed and any errors have occurred,
  543. // the NSArray returned in errorLog contains all error descriptions in the
  544. // order they ocurred.
  545. //
  546. // error-conditions:
  547. // If startTag is empty or nil, an exception TEStartTagEmptyOrNil will be
  548. // raised. If end tag is empty or nil, an exception TEEndTagEmptyOrNil will
  549. // be raised. It is the responsibility of the caller to catch the exception.
  550. + (id)stringByExpandingTemplate:(NSString *)templateString
  551. withStartTag:(NSString *)startTag
  552. andEndTag:(NSString *)endTag
  553. usingDictionary:(NSDictionary *)dictionary
  554. errorsReturned:(NSArray **)errorLog
  555. {
  556. NSArray *template = [templateString arrayBySeparatingLinesUsingEOLmarkers];
  557. NSEnumerator *list = [template objectEnumerator];
  558. NSMutableString *result = [NSMutableString stringWithCapacity:[templateString length]];
  559. NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet];
  560. NSMutableDictionary *_dictionary = [NSMutableDictionary dictionaryWithDictionary:dictionary];
  561. NSProcessInfo *processInfo = [NSProcessInfo processInfo];
  562. // IF/IF-NOT groups:
  563. // Each if-directive is processed by the same code as the directive's
  564. // complement directive. For example, %IF and %IFNOT are processed by the
  565. // same code in the parser. In order to be able to determine whether the
  566. // directive was a complement or not, a complement flag is used. Upon entry
  567. // into the parser loop, the complement flag is set whenever an %IFNOT,
  568. // %IFNEQ, %IFNDEF, %ELSIFNEQ or %ELSIFNDEF directive is found, and it is
  569. // cleared whenever an %IF, %IFEQ, %IFDEF, %ELSIFEQ or %ELSIFDEF is found.
  570. // When evaluating the expression following an if- or else-if directive a
  571. // logical XOR of the complement flag is applied to the expression.
  572. unsigned complement = 0;
  573. // Nested %IF blocks:
  574. // When a new %IF block is opened by %IF, %IFEQ, %IFNEQ, %IFDEF or %IFNDEF,
  575. // the current state held in flags is saved to stack and a new set of flags
  576. // is initialised. When an open if-block is closed by %ENDIF, the state of
  577. // the previous if-block is restored from stack to flags.
  578. TEFlags *flags = [TEFlags newFlags];
  579. LIFO *stack = [LIFO stackWithCapacity:8];
  580. // Error log:
  581. NSMutableArray *_errorLog = [NSMutableArray arrayWithCapacity:5];
  582. #define errorsHaveOcurred ([_errorLog count] > 0)
  583. NSMutableArray *lineErrors = [NSMutableArray arrayWithCapacity:2];
  584. #define lineErrorsHaveOcurred ([lineErrors count] > 0)
  585. TEError *error;
  586. // Temporary string variables and line counter
  587. NSString *line, *remainder, *keyword, *key, *value, *operand;
  588. unsigned len, lineNumber = 0;
  589. // -----------------------------------------------------------------------
  590. // P r e c o n d i t i o n s c h e c k
  591. // -----------------------------------------------------------------------
  592. NSException *exception;
  593. // check if start tag is nil or empty
  594. if ((startTag == nil) || ([startTag length] == 0)) {
  595. // this is a fatal error -- bail out by raising an exception
  596. [NSException exceptionWithName:@"TEStartTagEmptyOrNil"
  597. reason:@"startTag is empty or nil" userInfo:nil];
  598. [exception raise];
  599. } // end if
  600. // check if end tag is nil or empty
  601. if ((endTag == nil) || ([endTag length] == 0)) {
  602. // this is a fatal error -- bail out by raising an exception
  603. [NSException exceptionWithName:@"TEEndTagEmptyOrNil"
  604. reason:@"endTag is empty or nil" userInfo:nil];
  605. [exception raise];
  606. } // end if
  607. // -----------------------------------------------------------------------
  608. // A u t o m a t i c p l a c e h o l d e r v a r i a b l e s
  609. // -----------------------------------------------------------------------
  610. // Entering automatic placeholder variables into the dictionary:
  611. [_dictionary setObject:[[NSDate date] description] forKey:@"_timestamp"];
  612. [_dictionary setObject:[processInfo globallyUniqueString] forKey:@"_uniqueID"];
  613. [_dictionary setObject:[processInfo hostName] forKey:@"_hostname"];
  614. // -----------------------------------------------------------------------
  615. // L o c a l e i n f o r m a t i o n
  616. // -----------------------------------------------------------------------
  617. // Sometimes Apple have got their priorities dead wrong. Without ugly
  618. // hacks this information is only available as of MacOS X 10.4 "Tiger".
  619. // Define locale specific variables if the NSLocale class is available ...
  620. if (classExists(@"NSLocale")) {
  621. NSLocale *locale;
  622. // user's locale settings
  623. locale = [NSLocale currentLocale];
  624. [_dictionary setObject:[locale objectForKey:NSLocaleCountryCode] forKey:@"_userCountryCode"];
  625. [_dictionary setObject:[locale objectForKey:NSLocaleLanguageCode] forKey:@"_userLanguage"];
  626. // system locale settings
  627. locale = [NSLocale systemLocale];
  628. key = [locale objectForKey:NSLocaleCountryCode];
  629. // if NSLocaleCountryCode is undefined for the system locale ...
  630. if (key == nil) {
  631. // set the variable to empty string
  632. [_dictionary setObject:kEmptyString forKey:@"_systemCountryCode"];
  633. }
  634. else {
  635. // set the variable to the value of NSLocaleCountryCode
  636. [_dictionary setObject:key forKey:@"_systemCountryCode"];
  637. } // end if
  638. key = [locale objectForKey:NSLocaleLanguageCode];
  639. // if NSLocaleLanguageCode is undefined for the system locale ...
  640. if (key == nil) {
  641. // set the variable to empty string
  642. [_dictionary setObject:kEmptyString forKey:@"_systemLanguage"];
  643. }
  644. else {
  645. // set the variable to the value of NSLocaleLanguageCode
  646. [_dictionary setObject:key forKey:@"_systemLanguage"];
  647. } // end if
  648. } // end if
  649. // -----------------------------------------------------------------------
  650. // P a r s e r l o o p
  651. // -----------------------------------------------------------------------
  652. while (line = [list nextObject]) {
  653. lineNumber++;
  654. // if the line begins with a % character but not the start tag ...
  655. if (([line hasPrefix:startTag] == NO) && ([line characterAtIndex:0] == '%')) {
  656. // then the first word is likely to be a keyword
  657. keyword = [line firstWordUsingDelimitersFromSet:whitespaceSet];
  658. // if keyword starts with "%IFN" or "%ELSIFN" set complement to 1, otherwise 0
  659. complement = (([keyword hasPrefix:@"%IFN"]) || ([keyword hasPrefix:@"%ELSIFN"]));
  660. // ---------------------------------------------------------------
  661. // % I F a n d % I F N O T b r a n c h
  662. // ---------------------------------------------------------------
  663. if (([keyword isEqualToString:@"%IF"]) || ([keyword isEqualToString:@"%IFNOT"])) {
  664. if (flags->expand) {
  665. // we are to evaluate if the key's value represents 'true'
  666. // evaluate expression following %IF/%IFNOT
  667. key = [line wordAtIndex:2 usingDelimitersFromSet:whitespaceSet];
  668. // if there is no identifier following %IF/%IFNOT ...
  669. if ([key isEmpty]) {
  670. // this is an error - create a new error description
  671. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  672. inLine:lineNumber atToken:(TE_IF + complement)];
  673. // and add this error to the error log
  674. [_errorLog addObject:error];
  675. // log this error to the console
  676. [error logErrorMessageForTemplate:kEmptyString];
  677. // *** we are going to ignore this entire if-elsif-else-endif block ***
  678. // if this is a nested if-else block ...
  679. if (flags->condex) {
  680. // save flags to the stack
  681. [stack pushObject:flags];
  682. // and initialise a new set of flags
  683. flags = [TEFlags newFlags];
  684. } // end if
  685. // clear the expand flag to ignore this if-branch
  686. flags->expand = false;
  687. // set the consumed flag to ignore any elsif- and else- branches
  688. flags->consumed = true;
  689. // set condex flag to indicate we're inside an if-else block
  690. flags->condex = true;
  691. }
  692. // if there is an identifier following %IF/%IFNOT ...
  693. else {
  694. // look up the value of the key in the dictionary
  695. value = [_dictionary valueForKey:key];
  696. // *** this is the surviving branch - others are error branches ***
  697. // if this is a nested if-else block ...
  698. if (flags->condex) {
  699. // save flags to the stack
  700. [stack pushObject:flags];
  701. // and initialise a new set of flags
  702. flags = [TEFlags newFlags];
  703. } // end if
  704. // evaluate if the value of the key represents 'true'
  705. flags->expand = ([value representsTrue]) ^ complement;
  706. // remember evaluation
  707. flags->consumed = flags->expand;
  708. // remember that we're in an if-else block
  709. flags->condex = true;
  710. } // end if
  711. } // end if
  712. }
  713. // ---------------------------------------------------------------
  714. // % E L S I F a n d % E L S I F N O T b r a n c h
  715. // ---------------------------------------------------------------
  716. else if (([keyword isEqualToString:@"%ELSIF"]) || ([keyword isEqualToString:@"%ELSIFNOT"])) {
  717. if (flags->condex) {
  718. // if any branch in this if-else block was true ...
  719. if (flags->consumed) {
  720. // do not evaluate
  721. flags->expand = false;
  722. }
  723. else {
  724. // evaluate expression following %ELSIF/%ELSIFNOT
  725. key = [line wordAtIndex:2 usingDelimitersFromSet:whitespaceSet];
  726. // if there is no identifier following %ELSIF/%ELSIFNOT ...
  727. if ([key isEmpty]) {
  728. // this is an error - create a new error description
  729. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  730. inLine:lineNumber atToken:(TE_ELSIF + complement)];
  731. // and add this error to the error log
  732. [_errorLog addObject:error];
  733. // log this error to the console
  734. [error logErrorMessageForTemplate:kEmptyString];
  735. // clear the expand flag to ignore this elsif-branch
  736. flags->expand = false;
  737. }
  738. else {
  739. // evaluate if the value of the key represents 'true'
  740. flags->expand = ([value representsTrue]) ^ complement;
  741. } // end if
  742. } // end if
  743. // remember evaluation
  744. flags->consumed = (flags->consumed || flags->expand);
  745. }
  746. else {
  747. // found %ELSIF/%ELSIFNOT without prior %IF block having been opened
  748. // this is an error - create a new error description
  749. error = [TEError error:TE_UNEXPECTED_TOKEN_ERROR
  750. inLine:lineNumber atToken:(TE_ELSIF + complement)];
  751. // and add this error to the error log
  752. [_errorLog addObject:error];
  753. // log this error to the console
  754. [error logErrorMessageForTemplate:kEmptyString];
  755. // clear the expand flag to ignore this elsif-branch
  756. flags->expand = false;
  757. } // end if
  758. }
  759. // ---------------------------------------------------------------
  760. // % I F E Q a n d % I F N E Q b r a n c h
  761. // ---------------------------------------------------------------
  762. else if (([keyword isEqualToString:@"%IFEQ"]) || ([keyword isEqualToString:@"%IFNEQ"])) {
  763. if (flags->expand) {
  764. // we are to compare the key's value with an operand ...
  765. // evaluate expression following %IFEQ/%IFNEQ
  766. key = [line wordAtIndex:2 usingDelimitersFromSet:whitespaceSet];
  767. // if there is no identifier following %IFEQ/%IFNEQ ...
  768. if ([key isEmpty]) {
  769. // this is an error - create a new error description
  770. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  771. inLine:lineNumber atToken:(TE_IFEQ + complement)];
  772. // and add this error to the error log
  773. [_errorLog addObject:error];
  774. // log this error to the console
  775. [error logErrorMessageForTemplate:kEmptyString];
  776. // *** we are going to ignore this entire if-elsif-else-endif block ***
  777. // if this is a nested if-else block ...
  778. if (flags->condex) {
  779. // save flags to the stack
  780. [stack pushObject:flags];
  781. // and initialise a new set of flags
  782. flags = [TEFlags newFlags];
  783. } // end if
  784. // clear the expand flag to ignore this if-branch
  785. flags->expand = false;
  786. // set the consumed flag to ignore any elsif- and else- branches
  787. flags->consumed = true;
  788. // set condex flag to indicate we're inside an if-else block
  789. flags->condex = true;
  790. }
  791. // if there is an identifier following %IFEQ/%IFNEQ ...
  792. else {
  793. // look up the value of the key in the dictionary
  794. value = [_dictionary valueForKey:key];
  795. // get the remaining characters following the key
  796. remainder = [[line restOfWordsUsingDelimitersFromSet:whitespaceSet]
  797. restOfWordsUsingDelimitersFromSet:whitespaceSet];
  798. // check if we have an operand
  799. len = [remainder length];
  800. if (len == 0) {
  801. // this is an error - no operand to compare
  802. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  803. inLine:lineNumber atToken:TE_KEY];
  804. [error setLiteral:key];
  805. // and add this error to the error log
  806. [_errorLog addObject:error];
  807. // log this error to the console
  808. [error logErrorMessageForTemplate:kEmptyString];
  809. // *** we are going to ignore this entire if-elsif-else-endif block ***
  810. // if this is a nested if-else block ...
  811. if (flags->condex) {
  812. // save flags to the stack
  813. [stack pushObject:flags];
  814. // and initialise a new set of flags
  815. flags = [TEFlags newFlags];
  816. } // end if
  817. // clear the expand flag to ignore this if-branch
  818. flags->expand = false;
  819. // set the consumed flag to ignore any elsif- and else- branches
  820. flags->consumed = true;
  821. // set condex flag to indicate we're inside an if-else block
  822. flags->condex = true;
  823. }
  824. else {
  825. if (len == 1) {
  826. // only one character - use it as it is
  827. operand = [remainder copy];
  828. }
  829. else {
  830. // multiple characters left on the line
  831. // check if we have a quoted string using single quotes
  832. if ([remainder characterAtIndex:0] == '\'') {
  833. // get the characters enclosed by the single quotes
  834. operand = [remainder substringWithStringInSingleQuotes];
  835. // if there are no closing quotes
  836. if (operand == nil) {
  837. // assume EOL terminates the quoted string
  838. operand = [remainder substringFromIndex:1];
  839. } // end if
  840. }
  841. // alternatively, a quoted string using double quotes
  842. else if ([remainder characterAtIndex:0 == '"']) {
  843. // get the characters enclosed by the double quotes
  844. operand = [remainder substringWithStringInDoubleQuotes];
  845. // if there are no closing quotes
  846. if (operand == nil) {
  847. // assume EOL terminates the quoted string
  848. operand = [remainder substringFromIndex:1];
  849. } // end if
  850. }
  851. // otherwise if we don't have a quoted string
  852. else {
  853. // get the first word of the remaining characters on the line
  854. operand = [remainder firstWordUsingDelimitersFromSet:whitespaceSet];
  855. } // end if
  856. } // end if
  857. // *** this is the surviving branch - others are error branches ***
  858. // if this is a nested if-else block ...
  859. if (flags->condex) {
  860. // save flags to the stack
  861. [stack pushObject:flags];
  862. // and initialise a new set of flags
  863. flags = [TEFlags newFlags];
  864. } // end if
  865. // compare the value of the key to the operand
  866. flags->expand = ([value isEqualToString:operand] == YES) ^ complement;
  867. // remember evaluation
  868. flags->consumed = flags->expand;
  869. // remember that we're in an if-else block
  870. flags->condex = true;
  871. } // end if
  872. } // end if
  873. } // end if
  874. }
  875. // ---------------------------------------------------------------
  876. // % E L S I F E Q a n d % E L S I F N E Q b r a n c h
  877. // ---------------------------------------------------------------
  878. if (([keyword isEqualToString:@"%ELSIFEQ"]) || ([keyword isEqualToString:@"%ELSIFNEQ"])) {
  879. // we only care about this block if it is part of an open %IF
  880. if (flags->condex) {
  881. // ignore if already consumed
  882. if (flags->consumed) {
  883. // do not expand this block
  884. flags->expand = false;
  885. }
  886. else {
  887. // evaluate expression following %ELSIFEQ/%ELSIFNEQ
  888. key = [line wordAtIndex:2 usingDelimitersFromSet:whitespaceSet];
  889. // if there is no identifier following %ELSIFEQ/%ELSIFNEQ ...
  890. if ([key isEmpty]) {
  891. // this is an error - create a new error description
  892. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  893. inLine:lineNumber atToken:(TE_ELSIFEQ + complement)];
  894. // and add this error to the error log
  895. [_errorLog addObject:error];
  896. // log this error to the console
  897. [error logErrorMessageForTemplate:kEmptyString];
  898. // clear the expand flag to ignore this elsif-branch
  899. flags->expand = false;
  900. }
  901. else {
  902. // look up the value of the key in the dictionary
  903. value = [_dictionary valueForKey:key];
  904. // get the remaining characters following the key
  905. remainder = [[line restOfWordsUsingDelimitersFromSet:whitespaceSet]
  906. restOfWordsUsingDelimitersFromSet:whitespaceSet];
  907. // check if we have an operand
  908. len = [remainder length];
  909. if (len == 0) {
  910. // this is an error - no operand to compare
  911. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  912. inLine:lineNumber atToken:TE_KEY];
  913. [error setLiteral:key];
  914. // and add this error to the error log
  915. [_errorLog addObject:error];
  916. // log this error to the console
  917. [error logErrorMessageForTemplate:kEmptyString];
  918. // clear the expand flag to ignore this elsif-branch
  919. flags->expand = false;
  920. }
  921. else {
  922. if (len == 1) {
  923. // only one character - use it as it is
  924. operand = [remainder copy];
  925. }
  926. else {
  927. // multiple characters left on the line
  928. // check if we have a quoted string using single quotes
  929. if ([remainder characterAtIndex:0] == '\'') {
  930. // get the characters enclosed by the single quotes
  931. operand = [remainder substringWithStringInSingleQuotes];
  932. // if there are no closing quotes
  933. if (operand == nil) {
  934. // assume EOL terminates the quoted string
  935. operand = [remainder substringFromIndex:1];
  936. } // end if
  937. }
  938. // alternatively, a quoted string using double quotes
  939. else if ([remainder characterAtIndex:0 == '"']) {
  940. // get the characters enclosed by the double quotes
  941. operand = [remainder substringWithStringInDoubleQuotes];
  942. // if there are no closing quotes
  943. if (operand == nil) {
  944. // assume EOL terminates the quoted string
  945. operand = [remainder substringFromIndex:1];
  946. } // end if
  947. }
  948. // otherwise if we don't have a quoted string
  949. else {
  950. // get the first word of the remaining characters on the line
  951. operand = [remainder firstWordUsingDelimitersFromSet:whitespaceSet];
  952. } // end if
  953. } // end if
  954. // *** this is the surviving branch - others are error branches ***
  955. // compare the value of the key to the operand
  956. flags->expand = ([value isEqualToString:operand] == YES) ^ complement;
  957. // remember evaluation
  958. flags->consumed = flags->expand;
  959. } // end if
  960. } // end if
  961. } // end if
  962. }
  963. // if this block is not part of an open %IF ...
  964. else {
  965. // found %ELSIFEQ/%ELSIFNEQ without prior %IF block having been opened
  966. // this is an error - create a new error description
  967. error = [TEError error:TE_UNEXPECTED_TOKEN_ERROR
  968. inLine:lineNumber atToken:(TE_ELSIFEQ + complement)];
  969. // and add this error to the error log
  970. [_errorLog addObject:error];
  971. // log this error to the console
  972. [error logErrorMessageForTemplate:kEmptyString];
  973. // clear the expand flag to ignore this elsif-branch
  974. flags->expand = false;
  975. } // end if
  976. }
  977. // ---------------------------------------------------------------
  978. // % I F D E F a n d % I F N D E F b r a n c h
  979. // ---------------------------------------------------------------
  980. else if (([keyword isEqualToString:@"%IFDEF"]) || ([keyword isEqualToString:@"%IFNDEF"])) {
  981. // get the identifier following %IFDEF/%IFNDEF
  982. key = [line wordAtIndex:2 usingDelimitersFromSet:whitespaceSet];
  983. // if there is no identifier following %IFDEF/%IFNDEF ...
  984. if ([key isEmpty]) {
  985. // this is an error - create a new error description
  986. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  987. inLine:lineNumber atToken:(TE_IFDEF + complement)];
  988. // and add this error to the error log
  989. [_errorLog addObject:error];
  990. // log this error to the console
  991. [error logErrorMessageForTemplate:kEmptyString];
  992. // *** we are going to ignore this entire if-elsif-else-endif block ***
  993. // if this is a nested if-else block ...
  994. if (flags->condex) {
  995. // save flags to the stack
  996. [stack pushObject:flags];
  997. // and initialise a new set of flags
  998. flags = [TEFlags newFlags];
  999. } // end if
  1000. // clear the expand flag to ignore this if-branch
  1001. flags->expand = false;
  1002. // set the consumed flag to ignore any elsif- and else- branches
  1003. flags->consumed = true;
  1004. // set condex flag to indicate we're inside an if-els…

Large files files are truncated, but you can click here to view the full file