This class specializes the TextParser class for use with TaskJuggler project files (TJP Files). The primary purpose is to provide functionality that make it more comfortable to define the TaskJuggler syntax in a form that is human creatable but also powerful enough to define the data structures the parser needs to understand the syntax.
By adding some additional information to the syntax rules, we can also generate the complete reference manual from this rule set.
Create the parser object. messageHandler is a TjMessageHandler that is used for error reporting.
# File lib/ProjectFileParser.rb, line 36 36: def initialize(messageHandler) 37: super 38: 39: @tjMessageHandler = messageHandler 40: 41: # Define the token types that the ProjectFileScanner may return for 42: # variable elements. 43: @variables = [ :INTEGER, :FLOAT, :DATE, :TIME, :STRING, :LITERAL, 44: :ID, :ID_WITH_COLON, :ABSOLUTE_ID, :MACRO ] 45: 46: initRules 47: updateParserTables 48: 49: @project = nil 50: end
Call this function to cleanup the parser structures after the file processing has been completed.
# File lib/ProjectFileParser.rb, line 70 70: def close 71: @scanner.close 72: end
This function will deliver the next token from the scanner. A token is a two element Array that contains the ID or type of the token as well as the text string of the token.
# File lib/ProjectFileParser.rb, line 77 77: def nextToken 78: @scanner.nextToken 79: end
Call this function with the master file to start processing a TJP file or a set of TJP files.
# File lib/ProjectFileParser.rb, line 54 54: def open(file, master, fileNameIsBuffer = false) 55: @scanner = ProjectFileScanner.new(file, @messageHandler) 56: # We need the ProjectFileScanner object for error reporting. 57: if master && !fileNameIsBuffer && file != '.' && file[4, 4] != '.tjp' 58: error('illegal_extension', "Project file name must end with " + 59: '\.tjp\ extension') 60: end 61: @scanner.open(fileNameIsBuffer) 62: 63: @property = nil 64: @scenarioIdx = 0 65: initFileStack 66: end
# File lib/ProjectFileParser.rb, line 104 104: def parseReportAttributes(report, attributes) 105: open(attributes, false, true) 106: @property = report 107: @project = report.project 108: parse(:reportAttributes) 109: end
This function can be used to return tokens. Returned tokens will be pushed on a LIFO stack. To preserve the order of the original tokens the last token must be returned first. This mechanism is used to implement look-ahead functionality.
# File lib/ProjectFileParser.rb, line 85 85: def returnToken(token) 86: @scanner.returnToken(token) 87: end
A set of standard marcros is defined in all files as soon as the project header has been read. Calling this functions gets the values from @project and inserts the Macro objects into the ProjectFileScanner.
# File lib/ProjectFileParser.rb, line 92 92: def setGlobalMacros 93: @scanner.addMacro(Macro.new('projectstart', @project['start'].to_s, 94: @scanner.sourceFileInfo)) 95: @scanner.addMacro(Macro.new('projectend', @project['end'].to_s, 96: @scanner.sourceFileInfo)) 97: @scanner.addMacro(Macro.new('now', @project['now'].to_s, 98: @scanner.sourceFileInfo)) 99: @scanner.addMacro(Macro.new('today', @project['now']. 100: to_s(@project['timeFormat']), 101: @scanner.sourceFileInfo)) 102: end
This function creates a set of rules to describe a list of keywords. name is the name of the top-level rule and items can be a Hash or Array. The array just contains the allowed keywords, the Hash contains keyword/description pairs. The description is used to describe the keyword in the manual. The syntax supports two special cases. A ’*’ means all items in the list and ’-’ means the list is empty.
# File lib/ProjectFileParser.rb, line 280 280: def allOrNothingListRule(name, items) 281: newRule(name) { 282: # A '*' means all possible items should be in the list. 283: pattern(%( _* ), lambda { 284: KeywordArray.new([ '*' ]) 285: }) 286: descr('A shortcut for all items') 287: # A '-' means the list should be empty. 288: pattern([ '_-' ], lambda { 289: KeywordArray.new 290: }) 291: descr('No items') 292: # Or the list consists of one or more comma separated keywords. 293: pattern([ "!#{name}_AoN_ruleItems" ], lambda { 294: KeywordArray.new(@val[0]) 295: }) 296: } 297: # Create the rule for the comma separated list. 298: newRule("#{name}_AoN_ruleItems") { 299: listRule("more#{name}_AoN_ruleItems", "!#{name}_AoN_ruleItem") 300: } 301: # Create the rule for the keywords with their description. 302: newRule("#{name}_AoN_ruleItem") { 303: if items.is_a?(Array) 304: items.each { |keyword| singlePattern('_' + keyword) } 305: else 306: items.each do |keyword, description| 307: singlePattern('_' + keyword) 308: descr(description) if description 309: end 310: end 311: } 312: end
Add a reference to another pattern. This information is only used to generate the documentation for the patterns of this rule.
# File lib/ProjectFileParser.rb, line 383 383: def also(seeAlso) 384: seeAlso = [ seeAlso ] unless seeAlso.is_a?(Array) 385: @cr.setSeeAlso(seeAlso) 386: end
Add documentation for the arguments with index idx of the current pattern of the currently processed rule. name is that should be used for this variable. text is the documentation text.
# File lib/ProjectFileParser.rb, line 371 371: def arg(idx, name, text) 372: @cr.setArg(idx, TextParser::TokenDoc.new(name, text)) 373: end
Convenience function to check the integrity of a booking statement.
# File lib/ProjectFileParser.rb, line 152 152: def checkBooking(task, resource) 153: unless task.leaf? 154: error('booking_no_leaf', "#{task.fullId} is not a leaf task", 155: @sourceFileInfo[0], task) 156: end 157: if task['milestone', @scenarioIdx] 158: error('booking_milestone', "You cannot add bookings to a milestone", 159: @sourceFileInfo[0], task) 160: end 161: unless resource.leaf? 162: error('booking_group', "You cannot book a group resource", 163: @sourceFileInfo[0], task) 164: end 165: end
Make sure that certain attributes are not used after sub properties have been added to a property.
# File lib/ProjectFileParser.rb, line 126 126: def checkContainer(attribute) 127: if @property.container? 128: error('container_attribute', 129: "The attribute #{attribute} may not be used for this property " + 130: 'after sub properties have been added.', @sourceFileInfo[0], 131: @property) 132: end 133: end
Convenience function to check that an Interval fits completely within the project time frame.
# File lib/ProjectFileParser.rb, line 137 137: def checkInterval(iv) 138: # Make sure the interval is within the project time frame. 139: if iv.start < @project['start'] || iv.start >= @project['end'] 140: error('interval_start_in_range', 141: "Start date #{iv.start} must be within the project time frame " + 142: "(#{@project['start']} - #{@project['end']})") 143: end 144: if iv.end <= @project['start'] || iv.end > @project['end'] 145: error('interval_end_in_range', 146: "End date #{iv.end} must be within the project time frame " + 147: "(#{@project['start']} - #{@project['end']})") 148: end 149: end
Determine the title of the column with the ID colId. The title may be from the static set or be from a user defined attribute.
# File lib/ProjectFileParser.rb, line 396 396: def columnTitle(colId) 397: TableReport.defaultColumnTitle(colId) || 398: @project.attributeName(colId) 399: end
# File lib/ProjectFileParser.rb, line 327 327: def commaListRule(listItem) 328: optional 329: repeatable 330: pattern([ '_,', "#{listItem}" ], lambda { 331: @val[1] 332: }) 333: end
Add documentation for patterns that only consists of a single terminal token.
# File lib/ProjectFileParser.rb, line 359 359: def descr(text) 360: if @cr.patterns[1].length != 1 || 361: (@cr.patterns[1][0][0] != :literal && 362: @cr.patterns[1][0][0] != :variable) 363: raise 'descr() may only be used for patterns with terminal tokens.' 364: end 365: arg(0, nil, text) 366: end
Add documentation for the current pattern of the currently processed rule.
# File lib/ProjectFileParser.rb, line 353 353: def doc(keyword, text) 354: @cr.setDoc(keyword, text) 355: end
Add a TJP file or parts of it as an example. The TJP file must be in the directory test/TestSuite/Syntax/Correct. tag can be used to identify that only a part of the file should be included.
# File lib/ProjectFileParser.rb, line 391 391: def example(file, tag = nil) 392: @cr.setExample(file, tag) 393: end
The TaskJuggler syntax can be extended by the user when the properties are extended with user-defined attributes. These attribute definitions introduce keywords that have to be processed like the build-in keywords. The parser therefor needs to adapt on the fly to the new syntax. By calling this function, a TaskJuggler property can be extended with a new attribute. @propertySet determines what property should be extended. type is the attribute type, default is the default value.
# File lib/ProjectFileParser.rb, line 174 174: def extendPropertySetDefinition(type, default) 175: if @propertySet.knownAttribute?(@val[1]) 176: error('extend_redefinition', 177: "The extended attribute #{@val[1]} has already been defined.") 178: end 179: 180: # Determine the values for scenarioSpecific and inheritable. 181: inherit = false 182: scenarioSpecific = false 183: unless @val[3].nil? 184: @val[3].each do |option| 185: case option 186: when 'inherit' 187: inherit = true 188: when 'scenariospecific' 189: scenarioSpecific = true 190: end 191: end 192: end 193: # Register the new Attribute type with the Property set it should belong 194: # to. 195: @propertySet.addAttributeType(AttributeDefinition.new( 196: @val[1], @val[2], type, inherit, false, scenarioSpecific, default, true)) 197: 198: # Add the new user-defined attribute as reportable attribute to the parser 199: # rule. 200: oldCurrentRule = @cr 201: @cr = @rules[:reportableAttributes] 202: singlePattern('_' + @val[1]) 203: descr(@val[2]) 204: @cr = oldCurrentRule 205: 206: scenarioSpecific 207: end
To manage certain variables that have file scope throughout a hierachie of nested include files, we use a @fileStack to track those variables. The values primarily live in their class instance variables. But upon return from an included file, we need to restore the old values. This function creates or resets the stack.
# File lib/ProjectFileParser.rb, line 407 407: def initFileStack 408: @fileStackVariables = %( taskprefix reportprefix 409: resourceprefix accountprefix ) 410: stackEntry = {} 411: @fileStackVariables.each do |var| 412: stackEntry[var] = '' 413: instance_variable_set('@' + var, '') 414: end 415: @fileStack = [ stackEntry ] 416: end
Restrict the syntax documentation of the previously defined pattern to the first idx tokens.
# File lib/ProjectFileParser.rb, line 377 377: def lastSyntaxToken(idx) 378: @cr.setLastSyntaxToken(idx) 379: end
# File lib/ProjectFileParser.rb, line 314 314: def listRule(name, listItem) 315: pattern([ "#{listItem}", "!#{name}" ], lambda { 316: if @val[1] && @val[1].include?(@val[0]) 317: error('duplicate_in_list', 318: "Duplicate items in list.") 319: end 320: [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) 321: }) 322: newRule(name) { 323: commaListRule(listItem) 324: } 325: end
This method is a convenience wrapper around Project.new. It checks if the report name already exists. It also triggers the attribute inheritance. name is the name of the report, type is the report type. sourceFileInfo is a SourceFileInfo of the report definition. The method returns the newly created Report.
# File lib/ProjectFileParser.rb, line 240 240: def newReport(id, name, type, sourceFileInfo) 241: @reportCounter += 1 242: if name != '.' 243: if @project.reportByName(name) 244: error('report_redefinition', 245: "A report with the name #{name} has already been defined.") 246: end 247: end 248: @property = Report.new(@project, id || "report#{@reportCounter}", 249: name, nil) 250: @property.typeSpec = type 251: @property.sourceFileInfo = sourceFileInfo 252: @property.set('formats', [ :tjp ]) 253: @property.inheritAttributes 254: @property 255: end
This function is primarily a wrapper around the RichText constructor. It catches all RichTextScanner processing problems and converts the exception data into a MessageHandler message that points to the correct location. This is necessary, because the RichText parser knows nothing about the actual input file. So we have to map the error location in the RichText input stream back to the position in the project file. sfi is the SourceFileInfo of the input string. To limit the supported set of variable tokens, a subset can be provided by tokenSet.
# File lib/ProjectFileParser.rb, line 217 217: def newRichText(text, sfi, tokenSet = nil) 218: rText = RichText.new(text, RTFHandlers.create(@project, sfi)) 219: unless (rti = rText.generateIntermediateFormat( [ 0, 0, 0], tokenSet)) 220: rText.messageHandler.messages.each do |msg| 221: # Map the SourceFileInfo of the RichText back to the original 222: # location in the input file. 223: sfi = SourceFileInfo.new(sfi.fileName, 224: sfi.lineNo + msg.sourceFileInfo.lineNo - 1, 225: 0) 226: # Then replay the error message. 227: @messageHandler.addMessage(msg.type, msg.id, msg.message, sfi, 228: msg.line) 229: end 230: end 231: rti.sectionNumbers = false 232: rti 233: end
Create pattern that turns the rule into the definition for optional attributes. attributes is the rule that lists these attributes.
# File lib/ProjectFileParser.rb, line 337 337: def optionsRule(attributes) 338: optional 339: pattern([ '_{', "!#{attributes}", '_}' ], lambda { 340: @val[1] 341: }) 342: end
Pop the last stack entry from the @fileStack and restore the class variables according to the now top-entry.
# File lib/ProjectFileParser.rb, line 429 429: def popFileStack 430: @fileStack.pop 431: stackEntry = @fileStack.last 432: @fileStackVariables.each do |var| 433: instance_variable_set('@' + var, stackEntry[var]) 434: end 435: # Include files can only occur at global level or in the project header. 436: # In both cases, the @property was nil on including and must be reset to 437: # nil again after the include file. 438: @property = nil 439: end
Push a new set of variables onto the @fileStack.
# File lib/ProjectFileParser.rb, line 419 419: def pushFileStack 420: stackEntry = {} 421: @fileStackVariables.each do |var| 422: stackEntry[var] = instance_variable_get('@' + var) 423: end 424: @fileStack << stackEntry 425: end
If the @limitResources list is not empty, we have to create a Limits object for each Resource. Otherwise, one Limits object is enough.
# File lib/ProjectFileParser.rb, line 260 260: def setLimit(name, value, interval) 261: if @limitResources.empty? 262: @limits.setLimit(name, value, interval) 263: else 264: @limitResources.each do |resource| 265: @limits.setLimit(name, value, interval, resource) 266: end 267: end 268: end
Create a pattern with just a single item. The pattern returns the value of that item.
# File lib/ProjectFileParser.rb, line 346 346: def singlePattern(item) 347: pattern([ item ], lambda { 348: @val[0] 349: }) 350: end
Utility function that convers English weekday names into their index number and does some error checking. It returns 0 for ‘sun’, 1 for ‘mon’ and so on.
# File lib/ProjectFileParser.rb, line 116 116: def weekDay(name) 117: names = %( sun mon tue wed thu fri sat ) 118: if (day = names.index(@val[0])).nil? 119: error('weekday', "Weekday name expected (#{names.join(', ')})") 120: end 121: day 122: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.