home *** CD-ROM | disk | FTP | other *** search
/ Nebula 2 / Nebula Two.iso / SourceCode / MiscKit1.7.1 / MiscKit / Source / MiscMergeKit / MiscMergeCommand.m < prev    next >
Encoding:
Text File  |  1995-07-08  |  14.3 KB  |  427 lines

  1. //
  2. //    MiscMergeCommand.m -- abstract class to build merge commands from
  3. //        Written by Don Yacktman Copyright (c) 1995 by Don Yacktman.
  4. //                Version 1.0.  All rights reserved.
  5. //        This notice may not be removed from this source code.
  6. //
  7. //    This object is included in the MiscKit by permission from the author
  8. //    and its use is governed by the MiscKit license, found in the file
  9. //    "LICENSE.rtf" in the MiscKit distribution.  Please refer to that file
  10. //    for a list of all applicable permissions and restrictions.
  11. //    
  12.  
  13.  
  14. #import <misckit/MiscMergeCommand.h>
  15. #import <misckit/misckit.h>
  16.  
  17. @class MiscMergeEngine;
  18.  
  19. @interface MiscMergeCommand(private)
  20. - _getQuotedArgumentStringFrom:(MiscString *)aString toEnd:(BOOL)endFlag;
  21. @end
  22.  
  23. @implementation MiscMergeCommand
  24. /*" The MiscMergeCommand class implements a merge command.  Since the
  25. MiscKit merge engine can dynamically add new commands, it is possible
  26. to create custom subclasses of MiscMergeCommand to implement new
  27. functionality in the engine.  The merge engine already implements
  28. most of the commands a user would want, but certain applications may
  29. wish to override those commands, replace them, or add new commands.
  30.  
  31. To create a subclass, two methods, -#{parseFromString:} and
  32. -#{executeForMerge:} need to be implemented.  The first is expected to
  33. break up a text string into whatever arguments a particular MiscMergeCommand
  34. subclass needs in order to function.  The second performs, during a merge,
  35. whatever special task the command is designed to do.
  36.  
  37. The other methods in this object may be used by subclasses to aid in
  38. parsing the command string.  They can grab key words, conditional operators,
  39. an arguments (single word or quoted string).  A special kind of argument,
  40. promptable, is also supported.  A promptable argument is expected to have
  41. its actual value determined at run time.
  42.  
  43. When implementing commands, the full API of the MiscMergeEngine object is
  44. available.  This allows the programmer to store information in the engine,
  45. manipulate the symbol tables used for resolving fields, and alter the
  46. output being created by the merge.
  47.  
  48. The MiscKit source code is a good place to look for examples of how
  49. to implement various MiscMergeCommand subclasses.
  50. "*/
  51.  
  52. - init
  53. /*" Causes an error, since the -#{initFrom:} method %{must} be called
  54. to initialize MiscMergeCommands.  Frees self and returns nil.
  55. "*/
  56. {
  57.     [super init];
  58.     [self error:"Must use -initFrom: with MiscMergeCommand classes!\n"];
  59.     [self free];
  60.     return nil;
  61. }
  62.  
  63. - initFrom:aString
  64. /*" Initializes a new instance of MiscMergeCommand, based upon the text
  65. of %{aString}.  Actual parsing is performed by the -#{parseFromString:}
  66. method.  If parsing fails and returns nil, then this method will free
  67. the receiving instance and return nil.  Returns self otherwise.
  68. "*/
  69. {
  70.     [super init];
  71.     if (![self parseFromString:aString]) { // fatal parse error
  72.         [self free];
  73.         return nil;
  74.     }
  75.     return self;
  76. }
  77.  
  78. - parseFromString:(MiscString *)aString
  79. /*" This method is called while parsing a merge template.  The text of
  80. the full merge command is contained in %{aString}.  This method should
  81. break %{aString} up into keywords, conditionals, and arguments as
  82. needed and store the results in instance variables for later use during
  83. merges.  Note that returning self tells the template parsing machinery
  84. that all is well.  Return nil if there is an error or the command cannot
  85. be properly initialized.  (But do %{not} call -#{free} on self if nil
  86. is returned!)
  87. "*/
  88. {
  89.     // subclass responsibility
  90.     return nil;
  91. }
  92.  
  93. - executeForMerge:(MiscMergeEngine *)aMerger
  94. /*"  This method is called by the merge engine while it is
  95. performing a merge.  The command is expected to perform it's
  96. specified function when this call is received.  Returns self.  The
  97. return value is currently ignored by the caller, which is usually,
  98. but does not have to be, %{aMerger}.
  99. "*/
  100. {
  101.     // subclass responsibility
  102.     return nil;
  103. }
  104.  
  105. - (BOOL)eatKeyWord:(MiscString *)aKeyWord from:(MiscString *)aString
  106.     isOptional:(BOOL)flag
  107. /*" Attempts to remove the contents of %{aKeyWord} from %{aString}.
  108. If %{flag} is YES, then no complaint will be made if %{aKeyWord} is
  109. missing.  YES or NO is returned to tell the caller if the required
  110. key word was found or not, no matter what the value of %{flag} was.
  111. Note that it %{aKeyWord} is optional, then it will only be found
  112. if %{aString} has no whitespace at the beginning.  This is a design
  113. decision, since the merger has a few pseudo-commands, such as copy,
  114. which could give a less desireable behavior if the whitespace were
  115. trimmed from the start of %{aString}.  Since user-implemented
  116. MiscMergeCommand subclasses should pass YES for %{flag} this
  117. subtle design decision should not have a major impact.
  118. "*/
  119. {
  120. // If the keyword is optional, it may be implied, such as the "copy"
  121. // pseudo-command.  If that is the case, then we don't want to alter
  122. // the string at all.  So a copy command with the keyword in place
  123. // must not have any leading whitespace, since we won't attempt to
  124. // trim it.  This can leave unwanted keywords in the parse stream
  125. // if you aren't careful... This is a problem with the design and
  126. // not a bug.  This implementation will disturb things less than if
  127. // we always did a trim, so I prefer it.
  128.     if (!flag) [aString trimLeadWhiteSpaces];
  129.     if (![aString compareTo:aKeyWord n:[aKeyWord length]
  130.             caseSensitive:NO]) {
  131.         [aString removeFrom:0 length:[aKeyWord length]];
  132.         return YES;
  133.     }
  134.     if (!flag) {
  135.         [self error_keyword:aKeyWord];
  136.     }
  137.     return NO;
  138. }
  139.  
  140. - _getQuotedArgumentStringFrom:(MiscString *)aString toEnd:(BOOL)endFlag
  141. {
  142.     id ret = nil; int count = 0;
  143.  
  144.     [aString trimLeadWhiteSpaces];
  145.     if ([aString charAt:0] == '"') {
  146.         [aString removeFrom:0 length:1];
  147.     }
  148.  
  149.     if (endFlag) { // eat to the end, regardless.
  150.         [aString trimTailWhiteSpaces];
  151.         if ([aString charAt:([aString length]-1)] == '"') {
  152.             ret = [aString midFrom:0 length:([aString length]-1)];
  153.         } else {
  154.             [self error_closequote];
  155.             ret = [aString copy];
  156.         }
  157.         [aString setStringValue:""];
  158.         return ret;
  159.     }
  160.  
  161.     while (!ret) {
  162.         int right = [aString spotOf:'"'
  163.                 occurrenceNum:count caseSensitive:NO] - 1;
  164.         if (right < -1) {
  165.             ret = [aString copy];
  166.             [self error_closequote];
  167.         }
  168.         if (right == -1) {
  169.             ret = [MiscString new];
  170.             [self error_closequote];
  171.         }
  172.         if ([aString charAt:(right - 1)] == '\\') {
  173.             count += 1;
  174.         } else {
  175.             ret = [aString midFrom:0 to:right];
  176.         }
  177.     }
  178.     [aString replace:[ret stringValue] with:""];
  179.     return ret;
  180. }
  181.  
  182. - getArgumentStringFrom:(MiscString *)aString toEnd:(BOOL)endFlag
  183. /*" Attempts to parse an argument from %{aString}.  If %{endFlag}
  184. is set, then whatever is found to the end of %{aString} will be
  185. assumend to be the  required argument.  Otherwise, if an argument
  186. contains whitespace, it should be surrounded by quotation marks “"”.
  187. The parsed argument will be removed from %{aString}.
  188. "*/
  189. {
  190.     [aString trimLeadWhiteSpaces];
  191.     if ([aString charAt:0] == '"') {
  192.         return [self _getQuotedArgumentStringFrom:aString toEnd:endFlag];
  193.     } else {
  194.         if (!endFlag) {
  195.             id ret;
  196.             int brk = [aString spotOfChars:" \t\n<>!=" occurrenceNum:0];
  197.             if (brk <= 0) { // goes to end anyway
  198.                 id ret = [aString copy];
  199.                 [aString setStringValue:""];
  200.                 return ret;
  201.             }
  202.             ret = [aString midFrom:0 to:brk];
  203.             [aString replace:[ret stringValue] with:""];
  204.             return ret;
  205.         } else {
  206.             id ret = [aString copy];
  207.             [aString setStringValue:""];
  208.             return ret;
  209.         }
  210.     }
  211.     return nil;
  212. }
  213.  
  214. - getPromptFrom:(MiscString *)aString toEnd:(BOOL)endFlag
  215. /*" Attempts to parse a promptable argument from %{aString}.  If the
  216. argument begins with a "?" then the argument is a "prompt".  Removes
  217. the parsed argument from %{aString} and returns it.  Returns nil if
  218. the wrong kind of argument was found.  It is expected that a promptable
  219. argument's value will be determined at run time by asking the user for
  220. the value that should be stored for it.
  221. "*/
  222. {
  223.     [aString trimLeadWhiteSpaces];
  224.     if ([aString charAt:0] == '?') {
  225.         [aString removeFrom:0 length:1];
  226.         return [self getArgumentStringFrom:aString toEnd:endFlag];
  227.     } else {
  228.         [self error_noprompt];
  229.         return nil;
  230.     }
  231.     return nil;
  232. }
  233.  
  234. - getPromptableArgumentStringFrom:(MiscString *)aString
  235.         wasPrompt:(BOOL *)prompt toEnd:(BOOL)endFlag
  236. /*" Attempts to parse an argument, which could be promptable, from %{aString}.
  237. If the argument begins with a "?" then the argument is a "prompt".  Otherwise,
  238. a regular argument is parsed.  Removes the parsed argument from %{aString}
  239. and returns it.  %{prompt} is set to YES or NO depending upon what was parsed.
  240. "*/
  241. {
  242.     [aString trimLeadWhiteSpaces];
  243.     if ([aString charAt:0] == '?') {
  244.         *prompt = NO;
  245.         return [self getArgumentStringFrom:aString toEnd:endFlag];
  246.     } else {
  247.         *prompt = YES;
  248.         return [self getPromptFrom:aString toEnd:endFlag];
  249.     }
  250.     return nil;
  251. }
  252.  
  253. - (MISC_Merge_Cond_Op)getConditionalFrom:(MiscString *)aString
  254. /*" Attempts to parse a conditional from %{aString}.  Currently recognized
  255. conditionals are:  <>, ><, !=, <=, =<, >=, =>, <, >, ==, =.  Returns the
  256. type of conditional found or MISC_MCO_NONE if an unrecognized conditional
  257. is found.  Removes the parsed conditional from %{aString}.
  258. "*/
  259. {
  260.     MISC_Merge_Cond_Op operator = MISC_MCO_NONE;
  261.     id chars = [MiscString newWithString:"=!<> \t\n"];
  262.     id condString = [MiscString new];
  263.     [aString trimLeadWhiteSpaces];
  264.     while ([chars spotOf:[aString charAt:0]] >= 0) {
  265.         [condString addChar:[aString charAt:0]];
  266.         [aString removeFrom:0 length:1];
  267.     }
  268.     [condString replaceEveryOccurrenceOfChars:" \t\n" with:""];
  269.     if ([condString length] > 2) {
  270.         [self error_conditional:condString];
  271.         return MISC_MCO_NONE;
  272.     }
  273.     if ([condString length] < 1) return MISC_MCO_NONE;
  274.     if (![condString cmp:"<>" n:2]) {
  275.         operator = MISC_MCO_NOTEQUAL;
  276.     } else if (![condString cmp:"><" n:2]) {
  277.         operator = MISC_MCO_NOTEQUAL;
  278.     } else if (![condString cmp:"!=" n:2]) {
  279.         operator = MISC_MCO_NOTEQUAL;
  280.     } else if (![condString cmp:"<=" n:2]) {
  281.         operator = MISC_MCO_LESSTHANOREQUAL;
  282.     } else if (![condString cmp:">=" n:2]) {
  283.         operator = MISC_MCO_GREATERTHANOREQUAL;
  284.     } else if (![condString cmp:"=>" n:2]) {
  285.         operator = MISC_MCO_GREATERTHANOREQUAL;
  286.     } else if (![condString cmp:"=<" n:2]) {
  287.         operator = MISC_MCO_LESSTHANOREQUAL;
  288.     } else if (![condString cmp:">" n:1]) {
  289.         operator = MISC_MCO_GREATERTHAN;
  290.     } else if (![condString cmp:"<" n:1]) {
  291.         operator = MISC_MCO_LESSTHAN;
  292.     } else if (![condString cmp:"==" n:2]) {
  293.         operator = MISC_MCO_EQUAL;
  294.     } else if (![condString cmp:"=" n:1]) {
  295.         operator = MISC_MCO_EQUAL;
  296.     } else {
  297.         [self error_conditional:condString];
  298.         return MISC_MCO_NONE;
  299.     }
  300.     return operator;
  301. }
  302.  
  303. - (void)error_conditional:(MiscString *)theCond
  304. /*" This method is called if, while parsing, it is discovered that
  305. a conditional is unrecognized.  Prints the name of the merge
  306. command class, the text “Unrecognized conditional:”, and the -#{stringValue}
  307. of %{theCond} to the console.
  308. "*/
  309. {
  310.     fprintf(stderr, "%s:  Unrecognized conditional:  \"%s\".\n",
  311.             [[self class] name], [theCond stringValue]);
  312. }
  313.  
  314. - (void)error_keyword:(MiscString *)aKeyWord
  315. /*" This method is called if, while parsing, it is discovered that
  316. a required key word is missing.  Prints the name of the merge
  317. command class, the text “Missing key word:”, and the -#{stringValue}
  318. of %{aKeyWord} to the console.
  319. "*/
  320. {
  321.     fprintf(stderr, "%s:  Missing key word:  \"%s\".\n",
  322.             [[self class] name], [aKeyWord stringValue]);
  323. }
  324.  
  325. - (void)error_noprompt
  326. /*" This method is called if, while parsing, it is discovered that
  327. the required prompt is missing.  (Referring to arguments that are
  328. promptable.)  Prints the name of the merge
  329. command class and the text “Missing prompt.” to the console.
  330. "*/
  331. {
  332.     fprintf(stderr, "%s:  Missing prompt.\n", [[self class] name]);
  333. }
  334.  
  335. - (void)error_closequote
  336. /*" This method is called if, while parsing, it is discovered that
  337. quotations are not matched up properly.  Prints the name of the merge
  338. command class and the text “Closing quote missing or spurious extra
  339. argument added.” to the console.
  340. "*/
  341. {
  342.     fprintf(stderr,
  343.         "%s:  Closing quote missing or spurious extra argument added.\n",
  344.         [[self class] name]);
  345. }
  346.  
  347. // Note:  if aCommand doesn't implement the MiscMergeCondCallback protocol,
  348. // you'll get a NO back automatically.
  349. + (BOOL)evaluateConditionWith:(MiscMergeEngine *)anEngine
  350.         for:aCommand
  351. /*" Evaluates the condition in %{aCommand} in the context of the merge
  352. in progress in %{anEngine}.  Returns YES if the condition evaluated true
  353. and NO if not.  If %{aCommand} doesn't implement the MiscMergeCondCallback
  354. protocol, you'll get a NO back automatically.  This method should be used
  355. by MiscMergeCommand subclasses that need to evaluate conditionals as part
  356. of their task, such as the "if" command.
  357. "*/
  358. {
  359.     id v1, firstOperand, secondOperand;
  360.     int comparison;
  361.  
  362.     // return NO if the callback isn't implemented...
  363.     if (![aCommand conformsTo:@protocol(MiscMergeCondCallback)]) return NO;
  364.  
  365.     // we cache this return value to save us 4 calls...
  366.     v1 = [aCommand value1];
  367.  
  368.     // special evaluators for first argument
  369.     if (![v1 casecmp:"--NONE--"]) return NO;
  370.     if (![v1 casecmp:"--NO--"]) return NO;
  371.     if (![v1 casecmp:"--ALL--"]) return YES;
  372.     if (![v1 casecmp:"--YES--"]) return YES;
  373.  
  374.     // turn the operands into their field values; literals will
  375.     // come through unchanged...
  376.     firstOperand = [anEngine getField:v1];
  377.     secondOperand = [anEngine getField:[aCommand value2]];
  378.     // string compare is default, so do it first
  379.     comparison = [firstOperand compareTo:secondOperand];
  380.     // if both start with a digit, then we'll override with a numerical comp.
  381.     if ((NXIsDigit([firstOperand charAt:0]) ||
  382.                 ([firstOperand charAt:0] == '-')) &&
  383.             (NXIsDigit([secondOperand charAt:0]) ||
  384.                 ([secondOperand charAt:0] == '-'))) {
  385.         float firstOp = [firstOperand floatValue];
  386.         float secondOp = [secondOperand floatValue];
  387.         if (firstOp < secondOp) comparison = -1;
  388.         else if (firstOp > secondOp) comparison = 1;
  389.         else comparison = 0;
  390.     }
  391.     // now that we have comparison results, turn them into a YES/NO
  392.     // depending upon the chosen operator.
  393.     switch ([aCommand operator]) {
  394.         case MISC_MCO_NONE : { // true if field is non-empty
  395.             if ([firstOperand length] > 0) return YES;
  396.             break;
  397.         }
  398.         case MISC_MCO_EQUAL : {
  399.             if (!comparison) return YES;
  400.             break;
  401.         }
  402.         case MISC_MCO_NOTEQUAL : {
  403.             if (comparison) return YES;
  404.             break;
  405.         }
  406.         case MISC_MCO_LESSTHANOREQUAL : {
  407.             if (!comparison || (comparison < 0)) return YES;
  408.             break;
  409.         }
  410.         case MISC_MCO_GREATERTHANOREQUAL : {
  411.             if (!comparison || (comparison > 0)) return YES;
  412.             break;
  413.         }
  414.         case MISC_MCO_LESSTHAN : {
  415.             if (comparison < 0) return YES;
  416.             break;
  417.         }
  418.         case MISC_MCO_GREATERTHAN : {
  419.             if (comparison > 0) return YES;
  420.             break;
  421.         }
  422.     }
  423.     return NO;
  424. }
  425.  
  426. @end
  427.