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