PageRenderTime 78ms CodeModel.GetById 37ms 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
  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-else block
  1005. flags->condex = true;
  1006. }
  1007. // if there is an identifier following %IFDEF/%IFNDEF ...
  1008. else {
  1009. // if this is a nested if-else block ...
  1010. if (flags->condex) {
  1011. // this is a nested %IFDEF - save flags to the stack
  1012. [stack pushObject:flags];
  1013. // and initialise a new set of flags
  1014. flags = [TEFlags newFlags];
  1015. } // end if
  1016. // set expand flag to true if key is defined, false if undefined
  1017. flags->expand = (keyDefined(_dictionary, key)) ^ complement;
  1018. // remember evaluation
  1019. flags->consumed = flags->expand;
  1020. // remember that we're in an if-else block
  1021. flags->condex = true;
  1022. } // end if
  1023. }
  1024. // ---------------------------------------------------------------
  1025. // % E L S I F D E F a n d % E L S I F N D E F b r a n c h
  1026. // ---------------------------------------------------------------
  1027. else if (([keyword isEqualToString:@"%ELSIFDEF"]) || ([keyword isEqualToString:@"%ELSIFNDEF"])) {
  1028. if (flags->condex) {
  1029. // if any branch in this if-else block was true ...
  1030. if (flags->consumed) {
  1031. // do not evaluate
  1032. flags->expand = false;
  1033. }
  1034. else {
  1035. // evaluate expression following %ELSIFDEF/%ELSIFNDEF
  1036. key = [line wordAtIndex:2 usingDelimitersFromSet:whitespaceSet];
  1037. // if there is no identifier following %ELSIFDEF/%ELSIFNDEF
  1038. if ([key isEmpty]) {
  1039. // this is an error - create a new error description
  1040. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  1041. inLine:lineNumber atToken:(TE_ELSIFDEF + complement)];
  1042. // and add this error to the error log
  1043. [_errorLog addObject:error];
  1044. // log this error to the console
  1045. [error logErrorMessageForTemplate:kEmptyString];
  1046. // clear the expand flag to ignore this elsif-branch
  1047. flags->expand = false;
  1048. }
  1049. else {
  1050. // set expand flag to true if key is defined, false if undefined
  1051. flags->expand = (keyDefined(_dictionary, key)) ^ complement;
  1052. } // end if
  1053. } // end if
  1054. // remember evaluation
  1055. flags->consumed = (flags->consumed || flags->expand);
  1056. }
  1057. else {
  1058. // found %ELSIFDEF/%ELSIFNDEF without prior %IF block having been opened
  1059. // this is an error - create a new error description
  1060. error = [TEError error:TE_UNEXPECTED_TOKEN_ERROR
  1061. inLine:lineNumber atToken:(TE_ELSIFDEF + complement)];
  1062. // and add this error to the error log
  1063. [_errorLog addObject:error];
  1064. // log this error to the console
  1065. [error logErrorMessageForTemplate:kEmptyString];
  1066. // clear the expand flag to ignore this elsif-branch
  1067. flags->expand = false;
  1068. } // end if
  1069. }
  1070. // ---------------------------------------------------------------
  1071. // % E L S E b r a n c h
  1072. // ---------------------------------------------------------------
  1073. else if ([keyword isEqualToString:@"%ELSE"]) {
  1074. if (flags->condex) {
  1075. // if any branch in this if-else block was true ...
  1076. flags->expand = !(flags->consumed);
  1077. flags->consumed = true;
  1078. }
  1079. else {
  1080. // found %ELSE without any prior %IF block having been opened
  1081. // this is an error - create a new error description
  1082. error = [TEError error:TE_UNEXPECTED_TOKEN_ERROR
  1083. inLine:lineNumber atToken:TE_ELSE];
  1084. // and add this error to the error log
  1085. [_errorLog addObject:error];
  1086. // log this error to the console
  1087. [error logErrorMessageForTemplate:kEmptyString];
  1088. // clear the expand flag to ignore this else-branch
  1089. flags->expand = false;
  1090. } // end if
  1091. }
  1092. // ---------------------------------------------------------------
  1093. // % E N D I F b r a n c h
  1094. // ---------------------------------------------------------------
  1095. else if ([keyword isEqualToString:@"%ENDIF"]) {
  1096. if (flags->condex) {
  1097. // we're leaving the if-else block ...
  1098. // check if there were any enclosing blocks
  1099. if ([stack count] > 0) {
  1100. // we're in a nested if-block
  1101. // restore the flags for the enclosing block
  1102. flags = [stack popObject];
  1103. }
  1104. else {
  1105. // we're not in a nested if-block
  1106. // reset flags to start conditions
  1107. flags->expand = true;
  1108. flags->consumed = false;
  1109. flags->condex = false;
  1110. } // end if
  1111. }
  1112. else {
  1113. // found %ENDIF without prior %IF block having been opened
  1114. // this is an error - create a new error description
  1115. error = [TEError error:TE_UNEXPECTED_TOKEN_ERROR
  1116. inLine:lineNumber atToken:TE_ENDIF];
  1117. // and add this error to the error log
  1118. [_errorLog addObject:error];
  1119. // log this error to the console
  1120. [error logErrorMessageForTemplate:kEmptyString];
  1121. } // end if
  1122. }
  1123. // ---------------------------------------------------------------
  1124. // % D E F I N E b r a n c h
  1125. // ---------------------------------------------------------------
  1126. else if ([keyword isEqualToString:@"%DEFINE"]) {
  1127. if (flags->expand) {
  1128. // we are to define a new key ...
  1129. // evaluate expression following %DEFINE
  1130. key = [line wordAtIndex:2 usingDelimitersFromSet:whitespaceSet];
  1131. // if there is no identifier following %DEFINE
  1132. if ([key isEmpty]) {
  1133. // this is an error - create a new error description
  1134. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  1135. inLine:lineNumber atToken:TE_DEFINE];
  1136. // and add this error to the error log
  1137. [_errorLog addObject:error];
  1138. // log this error to the console
  1139. [error logErrorMessageForTemplate:kEmptyString];
  1140. }
  1141. else {
  1142. // obtain the value for this key
  1143. value = [[line restOfWordsUsingDelimitersFromSet:whitespaceSet]
  1144. restOfWordsUsingDelimitersFromSet:whitespaceSet];
  1145. // add the new key to the dictionary
  1146. [_dictionary setObject:[value copy] forKey:key];
  1147. } // end if
  1148. } // end if
  1149. }
  1150. // ---------------------------------------------------------------
  1151. // % U N D E F b r a n c h
  1152. // ---------------------------------------------------------------
  1153. else if ([keyword isEqualToString:@"%UNDEF"]) {
  1154. if (flags->expand) {
  1155. // evaluate expression following %UNDEF
  1156. key = [line wordAtIndex:2 usingDelimitersFromSet:whitespaceSet];
  1157. // if there is no identifier following %UNDEF
  1158. if ([key isEmpty]) {
  1159. // this is an error - create a new error description
  1160. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  1161. inLine:lineNumber atToken:TE_UNDEF];
  1162. // and add this error to the error log
  1163. [_errorLog addObject:error];
  1164. // log this error to the console
  1165. [error logErrorMessageForTemplate:kEmptyString];
  1166. }
  1167. else {
  1168. // remove this key from the dictionary
  1169. [_dictionary removeObjectForKey:key];
  1170. } // end if
  1171. } // end if
  1172. }
  1173. // ---------------------------------------------------------------
  1174. // % L O G b r a n c h
  1175. // ---------------------------------------------------------------
  1176. else if ([keyword isEqualToString:@"%LOG"]) {
  1177. if (flags->expand) {
  1178. // we are to log text/keys to the console ...
  1179. // evaluate expression following %LOG
  1180. key = [line wordAtIndex:2 usingDelimitersFromSet:whitespaceSet];
  1181. // if there is no identifier following %UNDEF
  1182. if ([key isEmpty]) {
  1183. // this is an error - create a new error description
  1184. error = [TEError error:TE_MISSING_IDENTIFIER_AFTER_TOKEN_ERROR
  1185. inLine:lineNumber atToken:TE_LOG];
  1186. // and add this error to the error log
  1187. [_errorLog addObject:error];
  1188. // log this error to the console
  1189. [error logErrorMessageForTemplate:kEmptyString];
  1190. }
  1191. else {
  1192. // lookup the value for this key in the dictionary
  1193. value = [_dictionary valueForKey:key];
  1194. // and log it to the console
  1195. NSLog(@"value for key '%@' is '%@'", key, value);
  1196. } // end if
  1197. } // end if
  1198. }
  1199. // ---------------------------------------------------------------
  1200. // % E C H O b r a n c h
  1201. // ---------------------------------------------------------------
  1202. else if ([keyword isEqualToString:@"%ECHO"]) {
  1203. if (flags->expand) {
  1204. // we are to log text/keys to stdout ...
  1205. TODO
  1206. // this is not implemented yet
  1207. error = [TEError error:TE_UNIMPLEMENTED_TOKEN_ERROR
  1208. inLine:lineNumber atToken:TE_ECHO];
  1209. // and add this error to the error log
  1210. [_errorLog addObject:error];
  1211. // log this error to the console
  1212. [error logErrorMessageForTemplate:kEmptyString];
  1213. } // end if
  1214. }
  1215. // ---------------------------------------------------------------
  1216. // % D E B U G b r a n c h
  1217. // ---------------------------------------------------------------
  1218. else if ([keyword isEqualToString:@"%DEBUG"]) {
  1219. if (flags->expand) {
  1220. // we are to enable/disable debug mode ...
  1221. TODO
  1222. // this is not implemented yet
  1223. error = [TEError error:TE_UNIMPLEMENTED_TOKEN_ERROR
  1224. inLine:lineNumber atToken:TE_DEBUG];
  1225. // and add this error to the error log
  1226. [_errorLog addObject:error];
  1227. // log this error to the console
  1228. [error logErrorMessageForTemplate:kEmptyString];
  1229. } // end if
  1230. }
  1231. // ---------------------------------------------------------------
  1232. // % c o m m e n t b r a n c h
  1233. // ---------------------------------------------------------------
  1234. // if the '%' character is not part of a placeholder/directive ...
  1235. else {
  1236. // then the current line is a template comment.
  1237. // it will not be expanded nor copied to the result
  1238. } // end if
  1239. }
  1240. // if line does not begin with a % character ...
  1241. // then it is neither comment nor template command
  1242. // if expand flag is set, expand it, otherwise ignore it
  1243. else if (flags->expand) {
  1244. // expand the line and add it to the result
  1245. [result appendString:[line stringByExpandingPlaceholdersWithStartTag:startTag
  1246. andEndTag:endTag
  1247. usingDictionary:_dictionary
  1248. errorsReturned:&lineErrors
  1249. lineNumber:lineNumber]];
  1250. [result appendString:kLineFeed];
  1251. // if there were any errors ...
  1252. if (lineErrorsHaveOcurred) {
  1253. // add the errors to the error log
  1254. [_errorLog addObjectsFromArray:lineErrors];
  1255. } // end if
  1256. } // end if
  1257. } // end while
  1258. if (flags->condex) {
  1259. // we've reached the end of the template without previous %IF block having been closed
  1260. // this is an error - create a new error description
  1261. error = [TEError error:TE_EXPECTED_ENDIF_BUT_FOUND_TOKEN_ERROR
  1262. inLine:lineNumber atToken:TE_EOF];
  1263. // and add this error to the error log
  1264. [_errorLog addObject:error];
  1265. // log this error to the console
  1266. [error logErrorMessageForTemplate:kEmptyString];
  1267. } // end if
  1268. // if there were any errors ...
  1269. if (errorsHaveOcurred) {
  1270. // pass the error log back to the caller
  1271. *errorLog = _errorLog;
  1272. // get rid of the following line after testing
  1273. NSLog(@"errors have ocurred while expanding placeholders in string");
  1274. }
  1275. // if there were no errors ...
  1276. else {
  1277. // pass nil in the errorLog back to the caller
  1278. *errorLog = nil;
  1279. } // end if
  1280. // return the result string
  1281. return [NSString stringWithString:result];
  1282. #undef errorsHaveOcurred
  1283. #undef lineErrorsHaveOcurred
  1284. } // end method
  1285. // ---------------------------------------------------------------------------
  1286. // Class Method: stringByExpandingTemplateAtPath:usingDictionary:
  1287. // encoding:errorsReturned:
  1288. // ---------------------------------------------------------------------------
  1289. //
  1290. // description:
  1291. // Invokes method stringByExpandingTemplateAtPath:withStartTag:andEndTag:
  1292. // usingDictionary:encoding:errorsReturned: with the template engine's
  1293. // default tags: startTag "%«" and endTag "»". This method is source file
  1294. // encoding-safe.
  1295. + (id)stringByExpandingTemplateAtPath:(NSString *)path
  1296. usingDictionary:(NSDictionary *)dictionary
  1297. encoding:(NSStringEncoding)enc
  1298. errorsReturned:(NSArray **)errorLog
  1299. {
  1300. return [NSString stringByExpandingTemplateAtPath:path
  1301. withStartTag:[NSString defaultStartTag]
  1302. andEndTag:[NSString defaultEndTag]
  1303. usingDictionary:dictionary
  1304. encoding:enc
  1305. errorsReturned:errorLog];
  1306. } // end method
  1307. // ---------------------------------------------------------------------------
  1308. // Class Method: stringByExpandingTemplateAtPath:withStartTag:andEndTag:
  1309. // usingDictionary:encoding:errorsReturned:
  1310. // ---------------------------------------------------------------------------
  1311. //
  1312. // description:
  1313. // Returns a new NSString made by expanding the template file at path. This
  1314. // method reads the template file specified in path interpreted in the string
  1315. // encoding specified by encoding and then it invokes class method
  1316. // stringByExpandingTemplate:withStartTag:andEndTag:usingDictionary:
  1317. // errorsReturned: to expand the template read from the template file.
  1318. //
  1319. // pre-conditions:
  1320. // path is a valid path to the template file to be expanded.
  1321. // startTag and endTag must not be empty strings and must not be nil.
  1322. // dictionary contains keys to be replaced by their respective values.
  1323. // encoding is a valid string encoding as defined by NSStringEncoding.
  1324. // errorLog is an NSArray passed by reference.
  1325. //
  1326. // post-conditions:
  1327. // Return value contains a new NSString made by expanding the template file
  1328. // at path with lines outside of %IF blocks expanded unconditionally and
  1329. // lines inside of %IF blocks expanded conditionally. If any errors are
  1330. // encountered while attempting to read the template file or during template
  1331. // expansion, errorLog will be set to an NSArray containing error
  1332. // descriptions of class TEError for each error or warning. If there were
  1333. // neither errors nor warnings during template expansion, errorLog is set to
  1334. // nil.
  1335. // Errors that prevent the template file to be found or read will cause
  1336. // the method to abort the attempt to expand the template.
  1337. // For a description of error recovery for other errors see method
  1338. // stringByExpandingTemplate:withStartTag:andEndTag:usingDictionary:
  1339. // errorsReturned:
  1340. //
  1341. // error-conditions:
  1342. // If startTag is empty or nil, an exception StartTagEmptyOrNil will be
  1343. // raised. If end tag is empty or nil, an exception EndTagEmptyOrNil will be
  1344. // raised. It is the responsibility of the caller to catch the exception.
  1345. // If the template file cannot be found or opened, or if there is an encoding
  1346. // error with the contents of the file, one or more error descriptions of
  1347. // class TEError are returned in errorLog.
  1348. + (id)stringByExpandingTemplateAtPath:(NSString *)path
  1349. withStartTag:(NSString *)startTag
  1350. andEndTag:(NSString *)endTag
  1351. usingDictionary:(NSDictionary *)dictionary
  1352. encoding:(NSStringEncoding)enc
  1353. errorsReturned:(NSArray **)errorLog
  1354. {
  1355. NSFileManager *fileMgr = [NSFileManager defaultManager];
  1356. NSString *templateString, *desc, *reason;
  1357. NSData *templateData;
  1358. NSError *fileError = [[NSError alloc] initWithNullError];
  1359. #define fileErrorOcurred ([fileError isNullError] == NO)
  1360. TEError *error;
  1361. // check if a path was specified
  1362. if ((path == nil) || ([path length] == 0)) {
  1363. // create error description - invalid path
  1364. error = [TEError error:TE_INVALID_PATH_ERROR inLine:0 atToken:TE_PATH];
  1365. [error setLiteral:@"(empty path)"];
  1366. // log this error to the console
  1367. [error logErrorMessageForTemplate:path];
  1368. // and add this error to the error log
  1369. *errorLog = [NSArray arrayWithObject:error];
  1370. // this error is not recoverable - abort
  1371. return nil;
  1372. } // end if
  1373. // check if path is an absolute path
  1374. path = [path stringByStandardizingPath];
  1375. if ([path isAbsolutePath] == NO) {
  1376. // create error description - invalid path
  1377. error = [TEError error:TE_INVALID_PATH_ERROR inLine:0 atToken:TE_PATH];
  1378. [error setLiteral:path];
  1379. // log this error to the console
  1380. [error logErrorMessageForTemplate:path];
  1381. // and add this error to the error log
  1382. *errorLog = [NSArray arrayWithObject:error];
  1383. // this error is not recoverable - abort
  1384. return nil;
  1385. } // end if
  1386. // if the specified file does not exist ...
  1387. if ([fileMgr fileExistsAtPath:path] == NO) {
  1388. // create error description - file not found
  1389. error = [TEError error:TE_FILE_NOT_FOUND_ERROR inLine:0 atToken:TE_PATH];
  1390. [error setLiteral:path];
  1391. // log this error to the console
  1392. [error logErrorMessageForTemplate:path];
  1393. // and add this error to the error log
  1394. *errorLog = [NSArray arrayWithObject:error];
  1395. // this error is not recoverable - abort
  1396. return nil;
  1397. }
  1398. // if the specified file is not readable ...
  1399. else if ([fileMgr isReadableFileAtPath:path] == NO) {
  1400. // create error description - cannot read file
  1401. error = [TEError error:TE_UNABLE_TO_READ_FILE_ERROR inLine:0 atToken:TE_PATH];
  1402. [error setLiteral:path];
  1403. // log this error to the console
  1404. [error logErrorMessageForTemplate:path];
  1405. // and add this error to the error log
  1406. *errorLog = [NSArray arrayWithObject:error];
  1407. // this error is not recoverable - abort
  1408. return nil;
  1409. }
  1410. // if the specified file is not a regular file nor a symlink pointing to a regular file ...
  1411. else if ([fileMgr isRegularFileAtPath:path] == NO) {
  1412. // create error description - not regular file
  1413. error = [TEError error:TE_UNABLE_TO_READ_FILE_ERROR inLine:0 atToken:TE_PATH];
  1414. [error setLiteral:path];
  1415. // log this error to the console
  1416. [error logErrorMessageForTemplate:path];
  1417. // and add this error to the error log
  1418. *errorLog = [NSArray arrayWithObject:error];
  1419. // this error is not recoverable - abort
  1420. return nil;
  1421. } // end if
  1422. if ([NSString respondsToSelector:@selector(stringWithContentsOfFile:encoding:error:)]) {
  1423. // use newer method as of MacOS X 10.4
  1424. templateString = [NSString stringWithContentsOfFile:path encoding:enc error:&fileError];
  1425. if (fileErrorOcurred) {
  1426. desc = [fileError localizedDescription];
  1427. reason = [fileError localizedFailureReason];
  1428. // if an error in the Cocoa Error Domain ocurred ...
  1429. if ([[fileError domain] isEqualToString:@"NSCocoaErrorDomain"]) {
  1430. // get the error code and match it to a corresponding TEError
  1431. switch([fileError code]) {
  1432. case NSFileNoSuchFileError :
  1433. case NSFileReadNoSuchFileError :
  1434. case NSFileReadInvalidFileNameError :
  1435. // create error description - file not found
  1436. error = [TEError error:TE_FILE_NOT_FOUND_ERROR inLine:0 atToken:TE_PATH];
  1437. break;
  1438. case NSFileReadNoPermissionError :
  1439. // create error description - cannot read file
  1440. error = [TEError error:TE_UNABLE_TO_READ_FILE_ERROR inLine:0 atToken:TE_PATH];
  1441. break;
  1442. case NSFileReadCorruptFileError :
  1443. // create error description - invalid file format
  1444. error = [TEError error:TE_INVALID_FILE_FORMAT_ERROR inLine:0 atToken:TE_PATH];
  1445. break;
  1446. case NSFileReadInapplicableStringEncodingError :
  1447. // create error description - encoding error
  1448. error = [TEError error:TE_TEMPLATE_ENCODING_ERROR inLine:0 atToken:TE_PATH];
  1449. break;
  1450. default :
  1451. // create error description - generic file error
  1452. error = [TEError error:TE_GENERIC_ERROR inLine:0 atToken:TE_PATH];
  1453. // embed the Cocoa generated error description
  1454. [error setLiteral:[NSString stringWithFormat:
  1455. @"while trying to access file at path %@\n%@\n%@", path, desc, reason]];
  1456. break;
  1457. } // end switch
  1458. }
  1459. // if the error is outside of the Cocoa Error Domain ...
  1460. else {
  1461. // create error description - generic file error
  1462. error = [TEError error:TE_GENERIC_ERROR inLine:0 atToken:TE_PATH];
  1463. // embed the Cocoa generated error description
  1464. [error setLiteral:[NSString stringWithFormat:
  1465. @"while trying to access file at path %@\n%@\n%@", path, desc, reason]];
  1466. } // end if
  1467. if ([[error literal] length] == 0) {
  1468. [error setLiteral:path];
  1469. } // end if
  1470. // log this error to the console
  1471. [error logErrorMessageForTemplate:path];
  1472. // and add this error to the error log
  1473. *errorLog = [NSArray arrayWithObject:error];
  1474. // this error is not recoverable - abort
  1475. return nil;
  1476. } // end if
  1477. }
  1478. else {
  1479. // use alternative method before MacOS X 10.4
  1480. templateData = [NSData dataWithContentsOfFile:path]; // path must be absolute
  1481. templateString = [[[NSString alloc] initWithData:templateData encoding:enc] autorelease];
  1482. if (false) { // how the heck do we know there was no encoding error?
  1483. // create error description - encoding error
  1484. error = [TEError error:TE_TEMPLATE_ENCODING_ERROR inLine:0 atToken:TE_PATH];
  1485. [error setLiteral:path];
  1486. // log this error to the console
  1487. [error logErrorMessageForTemplate:path];
  1488. // and add this error to the error log
  1489. *errorLog = [NSArray arrayWithObject:error];
  1490. // this error is not recoverable - abort
  1491. return nil;
  1492. } // end if
  1493. } // end if
  1494. NSLog(@"STS Template Engine - expanding template file %@ ...", path);
  1495. return [NSString stringByExpandingTemplate:templateString
  1496. withStartTag:startTag
  1497. andEndTag:endTag
  1498. usingDictionary:dictionary
  1499. errorsReturned:errorLog];
  1500. #undef fileErrorOcurred
  1501. } // end method
  1502. @end // STSTemplateEngine