class TaskJuggler::ProjectFileParser

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.

Public Class Methods

new() click to toggle source

Create the parser object.

Calls superclass method
# File lib/taskjuggler/ProjectFileParser.rb, line 34
def initialize
  super

  # Define the token types that the ProjectFileScanner may return for
  # variable elements.
  @variables = [ :INTEGER, :FLOAT, :DATE, :TIME, :STRING, :LITERAL,
                 :ID, :ID_WITH_COLON, :ABSOLUTE_ID, :MACRO ]

  initRules
  updateParserTables

  @project = nil
end

Public Instance Methods

close() click to toggle source

Call this function to cleanup the parser structures after the file processing has been completed.

# File lib/taskjuggler/ProjectFileParser.rb, line 69
def close
  @scanner.close
end
nextToken() click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 76
def nextToken
  @scanner.nextToken
end
open(file, master, fileNameIsBuffer = false) click to toggle source

Call this function with the master file to start processing a TJP file or a set of TJP files.

# File lib/taskjuggler/ProjectFileParser.rb, line 50
def open(file, master, fileNameIsBuffer = false)
  @scanner = ProjectFileScanner.new(file)
  # We need the ProjectFileScanner object for error reporting.
  if master && !fileNameIsBuffer && file != '.' && file[-4, 4] != '.tjp'
    error('illegal_extension', "Project file name must end with " +
          '\.tjp\ extension')
  end
  @scanner.open(fileNameIsBuffer)

  @property = nil
  @scenarioIdx = 0
  initFileStack
  # Stack for property IDs. Needed to handle nested 'supplement'
  # statements.
  @idStack = []
end
parseReportAttributes(report, attributes) click to toggle source
# File lib/taskjuggler/ProjectFileParser.rb, line 103
def parseReportAttributes(report, attributes)
  open(attributes, false, true)
  @property = report
  @project = report.project
  parse(:reportAttributes)
end
returnToken(token) click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 84
def returnToken(token)
  @scanner.returnToken(token)
end
setGlobalMacros() click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 91
def setGlobalMacros
  @scanner.addMacro(Macro.new('projectstart', @project['start'].to_s,
                              @scanner.sourceFileInfo))
  @scanner.addMacro(Macro.new('projectend', @project['end'].to_s,
                              @scanner.sourceFileInfo))
  @scanner.addMacro(Macro.new('now', @project['now'].to_s,
                              @scanner.sourceFileInfo))
  @scanner.addMacro(Macro.new('today', @project['now'].
                               to_s(@project['timeFormat']),
                              @scanner.sourceFileInfo))
end

Private Instance Methods

allOrNothingListRule(name, items) click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 320
def allOrNothingListRule(name, items)
  newRule(name) {
    # A '*' means all possible items should be in the list.
    pattern(%w( _* ), lambda {
      KeywordArray.new([ '*' ])
    })
    descr('A shortcut for all items')
    # A '-' means the list should be empty.
    pattern([ '_-' ], lambda {
      KeywordArray.new
    })
    descr('No items')
    # Or the list consists of one or more comma separated keywords.
    pattern([ "!#{name}_AoN_ruleItems" ], lambda {
      KeywordArray.new(@val[0])
    })
  }
  # Create the rule for the comma separated list.
  newRule("#{name}_AoN_ruleItems") {
    listRule("more#{name}_AoN_ruleItems", "!#{name}_AoN_ruleItem")
  }
  # Create the rule for the keywords with their description.
  newRule("#{name}_AoN_ruleItem") {
    if items.is_a?(Array)
      items.each { |keyword| singlePattern('_' + keyword) }
    else
      items.each do |keyword, description|
        singlePattern('_' + keyword)
        descr(description) if description
      end
    end
  }
end
also(seeAlso) click to toggle source

Add a reference to another pattern. This information is only used to generate the documentation for the patterns of this rule.

# File lib/taskjuggler/ProjectFileParser.rb, line 428
def also(seeAlso)
  seeAlso = [ seeAlso ] unless seeAlso.is_a?(Array)
  @cr.setSeeAlso(seeAlso)
end
appendScListAttribute(attrId, list) click to toggle source

This method most be used instead of the += operator for all list attributes. += will always return an Array object. This will cause trouble with the list attributes that are not plain Arrays.

# File lib/taskjuggler/ProjectFileParser.rb, line 492
def appendScListAttribute(attrId, list)
  list.each do |v|
    @property[attrId, @scenarioIdx] << v
  end
  # The << operator does not set the 'provided' flag. Just do a self
  # assignment to trigget the flag to get set.
  begin
    @property[attrId, @scenarioIdx] = @property[attrId, @scenarioIdx]
  rescue AttributeOverwrite
    # Overwrites are ok here.
  end
end
arg(idx, name, text) click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 411
def arg(idx, name, text)
  @cr.setArg(idx, TextParser::TokenDoc.new(name, text))
end
checkBooking(task, resource) click to toggle source

Convenience function to check the integrity of a booking statement.

# File lib/taskjuggler/ProjectFileParser.rb, line 151
def checkBooking(task, resource)
  unless task.leaf?
    error('booking_no_leaf', "#{task.fullId} is not a leaf task",
          @sourceFileInfo[0], task)
  end
  if task['milestone', @scenarioIdx]
    error('booking_milestone', "You cannot add bookings to a milestone",
          @sourceFileInfo[0], task)
  end
  unless resource.leaf?
    error('booking_group', "You cannot book a group resource",
          @sourceFileInfo[0], task)
  end
end
checkContainer(attribute) click to toggle source

Make sure that certain attributes are not used after sub properties have been added to a property.

# File lib/taskjuggler/ProjectFileParser.rb, line 125
def checkContainer(attribute)
  if @property.container?
    error('container_attribute',
          "The attribute #{attribute} may not be used for this property " +
          'after sub properties have been added.', @sourceFileInfo[0],
          @property)
  end
end
checkInterval(iv) click to toggle source

Convenience function to check that an TimeInterval fits completely within the project time frame.

# File lib/taskjuggler/ProjectFileParser.rb, line 136
def checkInterval(iv)
  # Make sure the interval is within the project time frame.
  if iv.start < @project['start'] || iv.start >= @project['end']
    error('interval_start_in_range',
          "Start date #{iv.start} must be within the project time frame " +
          "(#{@project['start']} - #{@project['end']})")
  end
  if iv.end <= @project['start'] || iv.end > @project['end']
    error('interval_end_in_range',
          "End date #{iv.end} must be within the project time frame " +
          "(#{@project['start']} - #{@project['end']})")
  end
end
columnTitle(colId) click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 441
def columnTitle(colId)
  if @property.typeSpec == :tracereport
    "<-id->:<-scenario->.#{colId}"
  else
    TableReport.defaultColumnTitle(colId) ||
      @project.attributeName(colId)
  end
end
commaListRule(listItem) click to toggle source
# File lib/taskjuggler/ProjectFileParser.rb, line 367
def commaListRule(listItem)
  optional
  repeatable
  pattern([ '_,', "#{listItem}" ], lambda {
    @val[1]
  })
end
descr(text) click to toggle source

Add documentation for patterns that only consists of a single terminal token.

# File lib/taskjuggler/ProjectFileParser.rb, line 399
def descr(text)
  if @cr.patterns[-1].length != 1 ||
     (@cr.patterns[-1][0][0] != :literal &&
      @cr.patterns[-1][0][0] != :variable)
    raise 'descr() may only be used for patterns with terminal tokens.'
  end
  arg(0, nil, text)
end
doc(keyword, text) click to toggle source

Add documentation for the current pattern of the currently processed rule.

# File lib/taskjuggler/ProjectFileParser.rb, line 393
def doc(keyword, text)
  @cr.setDoc(keyword, text)
end
example(file, tag = nil) click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 436
def example(file, tag = nil)
  @cr.setExample(file, tag)
end
extendPropertySetDefinition(type, default) click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 173
def extendPropertySetDefinition(type, default)
  if @propertySet.knownAttribute?(@val[1])
    error('extend_redefinition',
          "The extended attribute #{@val[1]} has already been defined.")
  end

  # Determine the values for scenarioSpecific and inheritable.
  inherit = false
  scenarioSpecific = false
  unless @val[3].nil?
    @val[3].each do |option|
      case option
      when 'inherit'
        inherit = true
      when 'scenariospecific'
        scenarioSpecific = true
      end
    end
  end
  # Register the new Attribute type with the Property set it should belong
  # to.
  @propertySet.addAttributeType(AttributeDefinition.new(
    @val[1], @val[2], type, inherit, false, scenarioSpecific, default,
    true))

  # Add the new user-defined attribute as reportable attribute to the parser
  # rule.
  oldCurrentRule = @cr
  @cr = @rules[:reportableAttributes]
  unless @cr.include?(@val[1])
    singlePattern('_' + @val[1])
    descr(@val[2])
  end
  @cr = oldCurrentRule

  scenarioSpecific
end
initFileStack() click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 456
def initFileStack
  @fileStackVariables = %w( taskprefix reportprefix
                            resourceprefix accountprefix )
  stackEntry = {}
  @fileStackVariables.each do |var|
    stackEntry[var] = ''
    instance_variable_set('@' + var, '')
  end
  @fileStack = [ stackEntry ]
end
lastSyntaxToken(idx) click to toggle source

Restrict the syntax documentation of the previously defined pattern to the first idx tokens.

# File lib/taskjuggler/ProjectFileParser.rb, line 417
def lastSyntaxToken(idx)
  @cr.setLastSyntaxToken(idx)
end
level(level) click to toggle source

Specify the support level for the current pattern.

# File lib/taskjuggler/ProjectFileParser.rb, line 422
def level(level)
  @cr.setSupportLevel(level)
end
listRule(name, listItem) click to toggle source
# File lib/taskjuggler/ProjectFileParser.rb, line 354
def listRule(name, listItem)
  pattern([ "#{listItem}", "!#{name}" ], lambda {
    if @val[1] && @val[1].include?(@val[0])
      error('duplicate_in_list',
            "Duplicate items in list.")
    end
    [ @val[0] ] + (@val[1].nil? ? [] : @val[1])
  })
  newRule(name) {
    commaListRule(listItem)
  }
end
newReport(id, name, type, sourceFileInfo) { || ... } click to toggle source

This method is a convenience wrapper around TaskJuggler::Report.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/taskjuggler/ProjectFileParser.rb, line 238
def newReport(id, name, type, sourceFileInfo)
  # If there is no parent property and the report prefix is not empty, the
  # reportprefix defines the parent property.
  if @property.nil? && !@reportprefix.empty?
    @property = @project.report(@reportprefix)
  end

  # Report IDs must be unique. If an ID was provided, check if it exists
  # already.
  if id
    # If we have a scope property, we need to prepend the ID of the scope
    # property to the provided ID.
    id = (@property ? @property.fullId + '.' : '') + @val[1]

    if @project.report(id)
      error('report_exists', "report #{id} has already been defined.",
            sourceFileInfo, @property)
    end
  end

  @reportCounter += 1
  if name != '.' && name != ''
    if @project.reportByName(name)
      error('report_redefinition',
            "A report with the name #{name} has already been defined.")
    end
  end
  @property = Report.new(@project, id || "report#{@reportCounter}",
                         name, @property)
  @property.typeSpec = type
  @property.sourceFileInfo = sourceFileInfo
  @property.inheritAttributes

  if block_given?
    # The default attribute values for this report type have to be set in
    # 'inherited' mode since they are not user provided.
    AttributeBase.setMode(1)
    yield
    AttributeBase.setMode(0)
  end
end
newRichText(text, sfi, tokenSet = nil) click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 219
def newRichText(text, sfi, tokenSet = nil)
  rText = RichText.new(text, RTFHandlers.create(@project, sfi))
  # The RichText is processed by a separate parser. Messages will not have
  # the proper source file info unless we baseline them with the original
  # source file info.
  mh = MessageHandlerInstance.instance
  mh.baselineSFI = sfi
  rti = rText.generateIntermediateFormat( [ 0, 0, 0 ], tokenSet)
  # Reset the baseline again.
  mh.baselineSFI = nil
  rti.sectionNumbers = false if rti
  rti
end
optionsRule(attributes) click to toggle source

Create pattern that turns the rule into the definition for optional attributes. attributes is the rule that lists these attributes.

# File lib/taskjuggler/ProjectFileParser.rb, line 377
def optionsRule(attributes)
  optional
  pattern([ '_{', "!#{attributes}", '_}' ], lambda {
    @val[1]
  })
end
popFileStack() click to toggle source

Pop the last stack entry from the @fileStack and restore the class variables according to the now top-entry.

# File lib/taskjuggler/ProjectFileParser.rb, line 478
def popFileStack
  stackEntry = @fileStack.pop
  @fileStackVariables.each do |var|
    instance_variable_set('@' + var, stackEntry[var])
  end
  # Include files can only occur at global level or in the project header.
  # In both cases, the @property was nil on including and must be reset to
  # nil again after the include file.
  @property = nil
end
pushFileStack() click to toggle source

Push a new set of variables onto the @fileStack.

# File lib/taskjuggler/ProjectFileParser.rb, line 468
def pushFileStack
  stackEntry = {}
  @fileStackVariables.each do |var|
    stackEntry[var] = instance_variable_get('@' + var)
  end
  @fileStack << stackEntry
end
setDurationAttribute(attribute, value = true) click to toggle source

Set the attribute to value and reset all other duration attributes.

# File lib/taskjuggler/ProjectFileParser.rb, line 293
def setDurationAttribute(attribute, value = true)
  checkContainer(attribute)
  { 'milestone' => false, 'duration' => 0,
    'length' => 0, 'effort' => 0 }.each do |attr, val|
    if attribute == attr
      @property[attr, @scenarioIdx] = value
    else
      if @property.getAttribute(attr, @scenarioIdx).provided
        error('multiple_durations',
              "This duration criteria is overwriting a previously " +
              "provided criteria (duration, effort, length or milestone).")
      end
      @property[attr, @scenarioIdx] = val
    end
  end
end
setLimit(name, value, interval) click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 282
def setLimit(name, value, interval)
  if @limitResources.empty?
    @limits.setLimit(name, value, interval)
  else
    @limitResources.each do |resource|
      @limits.setLimit(name, value, interval, resource)
    end
  end
end
singlePattern(item) click to toggle source

Create a pattern with just a single item. The pattern returns the value of that item.

# File lib/taskjuggler/ProjectFileParser.rb, line 386
def singlePattern(item)
  pattern([ item ], lambda {
    @val[0]
  })
end
weekDay(name) click to toggle source

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/taskjuggler/ProjectFileParser.rb, line 115
def weekDay(name)
  names = %w( sun mon tue wed thu fri sat )
  if (day = names.index(@val[0])).nil?
    error('weekday', "Weekday name expected (#{names.join(', ')})")
  end
  day
end