File: KeywordDocumentation.rb

package info (click to toggle)
tj3 3.8.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 5,048 kB
  • sloc: ruby: 36,481; javascript: 1,113; sh: 19; makefile: 17
file content (763 lines) | stat: -rw-r--r-- 25,100 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = KeywordDocumentation.rb -- The TaskJuggler III Project Management Software
#
# Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014
#               by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'term/ansicolor'
require 'taskjuggler/MessageHandler'
require 'taskjuggler/HTMLDocument'
require 'taskjuggler/RichText'
require 'taskjuggler/TjpExample'
require 'taskjuggler/TextFormatter'
require 'taskjuggler/Project'

class TaskJuggler

  # The textual TaskJuggler Project description consists of many keywords. The
  # parser has built-in support to document the meaning and usage of these
  # keywords. Most keywords are unique, but there can be exceptions. To
  # resolve ambiguoties the keywords can be prefixed by a scope. The scope is
  # usually a keyword that describes the context that the ambiguous keyword is
  # used in.  This class stores the keyword, the corresponding
  # TextParser::Pattern and the context that the keyword is used in. It also
  # stores information such as the list of optional attributes (keywords used
  # in the context of the current keyword) and whether the keyword is scenario
  # specific or not.
  class KeywordDocumentation

    include HTMLElements
    include Term::ANSIColor
    include MessageHandler

    attr_reader :keyword, :names, :pattern, :references, :optionalAttributes
    attr_accessor :contexts, :scenarioSpecific, :inheritedFromProject,
                  :inheritedFromParent, :predecessor, :successor

    # Construct a new KeywordDocumentation object. _rule_ is the
    # TextParser::Rule and _pattern_ is the corresponding TextParser::Pattern.
    # _syntax_ is an expanded syntax representation of the _pattern_. _args_
    # is an Array of TextParser::TokenDoc that describe the arguments of the
    # _pattern_.  _optAttrPatterns_ is an Array with references to
    # TextParser::Patterns that are optional attributes to this keyword.
    def initialize(rule, pattern, syntax, args, optAttrPatterns, manual)
      @rule = rule
      @pattern = pattern
      # The unique identifier. Usually the attribute or property name. To
      # disambiguate a .<scope> can be added.
      @keyword = pattern.keyword
      # Similar to @keyword, but without the scope. Since there could be
      # several, this is an Array of String objects.
      @names = []
      @syntax = syntax
      @args = args
      @manual = manual
      # Hash that maps patterns of optional attributes to a boolean value. It
      # is true if the pattern is a scenario specific attribute.
      @optAttrPatterns = optAttrPatterns
      # The above hash is later converted into a list that points to the
      # keyword documentation of the optional attribute.
      @optionalAttributes = []
      @scenarioSpecific = false
      @inheritedFromProject= false
      @inheritedFromParent = false
      @contexts = []
      @seeAlso = []
      # The following are references to the neighboring keyword in an
      # alphabetically sorted list.
      @predecessor = nil
      @successor = nil
      # Array to collect all references to other RichText objects.
      @references = []
    end

    # Returns true of the KeywordDocumentation is documenting a TJP property
    # (task, resources, etc.). A TJP property can be nested.
    def isProperty?
      # I haven't found a good way to automatically detect all the various
      # report types as properties. They don't directly include themselves as
      # attributes.
      return true if %w( accountreport export nikureport resourcereport
                         taskreport textreport timesheetreport
                         statussheetreport).include?(keyword)
      @optionalAttributes.include?(self)
    end

    # Returns true of the keyword can be used outside of any other keyword
    # context.
    def globalScope?
      return true if @contexts.empty?
      @contexts.each do |context|
        return true if context.keyword == 'properties'
      end
      false
    end

    # Post process the class member to set cross references to other
    # KeywordDocumentation items.
    def crossReference(keywords, rules)
      # Get the attribute or property name of the Keyword. This is not unique
      # like @keyword since it's got no scope.
      @pattern.terminalTokens(rules).each do |tok|
        # Ignore patterns that don't have a real name.
        break if tok[0] == '{'

        @names << tok[0]
      end

      # Some arguments are references to other patterns. The current keyword
      # is added as context to such patterns.
      @args.each do |arg|
        if arg.pattern && checkReference(arg.pattern)
          kwd = keywords[arg.pattern.keyword]
          kwd.contexts << self unless kwd.contexts.include?(self)
        end
      end

      # Optional attributes are treated similarly. In addition we add them to
      # the @optionalAttributes list of this keyword.
      @optAttrPatterns.each do |pattern, scenarioSpecific|
        next unless checkReference(pattern)

        # Check if all the attributes are documented. We ignore undocumented
        # keywords that are deprecated or removed.
        if (kwd = keywords[pattern.keyword]).nil?
          unless [ :deprecated, :removed ].include?(pattern.supportLevel)
            token = pattern.terminalTokens(rules)
            $stderr.puts "Keyword #{keyword} has undocumented optional " +
              "attribute #{token[0]}"
          end
        else
          @optionalAttributes << kwd
          kwd.contexts << self unless kwd.contexts.include?(self)
          kwd.scenarioSpecific = true if scenarioSpecific
        end
      end

      # Resolve the seeAlso patterns to keyword references.
      @pattern.seeAlso.sort.each do |also|
        if keywords[also].nil?
          raise "See also reference #{also} of #{@pattern} is unknown"
        end
        @seeAlso << keywords[also]
      end
    end

    def listAttribute?
      if (propertySet = findPropertySet)
        keyword = @keyword
        keyword = keyword.split('.')[0] if keyword.include?('.')
        return propertySet.listAttribute?(keyword)
      end

      false
    end

    def computeInheritance
      if (propertySet = findPropertySet)
        keyword = @keyword
        keyword = keyword.split('.')[0] if keyword.include?('.')
        @inheritedFromProject = propertySet.inheritedFromProject?(keyword)
        @inheritedFromParent = propertySet.inheritedFromParent?(keyword)
      end
    end

    # Return the keyword name in a more readable form. E.g. 'foo.bar' is
    # returned as 'foo (bar)'. 'foo' will remain 'foo'.
    def title
      kwTokens = @keyword.split('.')
      if kwTokens.size == 1
        title = @keyword
      else
        title = "#{kwTokens[0]} (#{kwTokens[1]})"
      end
      title
    end

    # Return the complete documentation of this keyword as formatted text
    # string.
    def to_s
      tagW = 13
      textW = 79

      # Top line with multiple elements
      str = "#{blue('Keyword:')}     #{bold(@keyword)}" +
            "#{listAttribute? ? ' (List Attribute)' : '' }\n\n"

      if @pattern.supportLevel != :supported
        msg = supportLevelMessage

        if [ :deprecated, :removed ].include?(@pattern.supportLevel) &&
           @seeAlso.length > 0
          msg += "\n\nPlease use "
          alsoStr = ''
          @seeAlso.each do |also|
            unless alsoStr.empty?
              alsoStr += ', '
            end
            alsoStr += also.keyword
          end
          msg += "#{alsoStr} instead!"
        end

        str += red("Warning:     #{format(tagW, msg, textW)}\n")
      end

      # Don't show further details if the keyword has been removed.
      return str if @pattern.supportLevel == :removed

      str += blue('Purpose:') +
             "     #{format(tagW, newRichText(@pattern.doc).to_s,
                                    textW)}\n"
      if @syntax != '[{ <attributes> }]'
        str += blue('Syntax:') + "      #{format(tagW, @syntax, textW)}\n"

        str += blue('Arguments:') + "   "
        if @args.empty?
          str += format(tagW, "none\n", textW)
        else
          argStr = ''
          @args.each do |arg|
            argText = newRichText(arg.text ||
              "See '#{arg.name}' for details.").to_s
            if arg.typeSpec.nil? || ("<#{arg.name}>") == arg.typeSpec
              indent = arg.name.length + 2
              argStr += "#{arg.name}: " +
                        "#{format(indent, argText, textW - tagW)}\n"
            else
              typeSpec = arg.typeSpec
              typeSpec[0] = '['
              typeSpec[-1] = ']'
              indent = arg.name.length + typeSpec.size + 3
              argStr += "#{arg.name} #{typeSpec}: " +
                        "#{format(indent, argText, textW - tagW)}\n"
            end
          end
          str += indent(tagW, argStr)
        end
        str += "\n"
      end

      str += blue('Context:') + '     '
      if @contexts.empty?
        str += format(tagW, 'Global scope', textW)
      else
        cxtStr = ''
        @contexts.each do |context|
          unless cxtStr.empty?
            cxtStr += ', '
          end
          cxtStr += context.keyword
        end
        str += format(tagW, cxtStr, textW)
      end

      str += "\n#{blue('Attributes:')}  "
      if @optionalAttributes.empty?
        str += "none\n\n"
      else
        attrStr = ''
        @optionalAttributes.sort! do |a, b|
          a.keyword <=> b.keyword
        end
        showLegend = false
        @optionalAttributes.each do |attr|
          unless attrStr.empty?
            attrStr += ', '
          end
          attrStr += attr.keyword
          if attr.scenarioSpecific || attr.inheritedFromProject ||
             attr.inheritedFromParent
            first = true
            showLegend = true
            tag = '['
            if attr.scenarioSpecific
              tag += 'sc'
              first = false
            end
            if attr.inheritedFromProject
              tag += ':' unless first
              tag += 'ig'
              first = false
            end
            if attr.inheritedFromParent
              tag += ':' unless first
              tag += 'ip'
            end
            tag += ']'
            attrStr += cyan(tag)
          end
        end
        if showLegend
          attrStr += "\n\n#{cyan('[sc]')} : Attribute is scenario specific" +
                     "\r#{cyan('[ig]')} : " +
                     "Value can be inherited from global setting" +
                     "\r#{cyan('[ip]')} : " +
                     "Value can be inherited from parent property"
        end
        str += format(tagW, attrStr, textW)
        str += "\n"
      end

      unless @seeAlso.empty?
        str += blue('See also:') + "    "
        alsoStr = ''
        @seeAlso.each do |also|
          unless alsoStr.empty?
            alsoStr += ', '
          end
          alsoStr += also.keyword
        end
        str += format(tagW, alsoStr, textW)
        str += "\n"
      end

  #    str += "Rule:    #{@rule.name}\n" if @rule
  #    str += "Pattern: #{@pattern.tokens.join(' ')}\n" if @pattern
      str
    end

    # Return a String that represents the keyword documentation in an XML
    # formatted form.
    def generateHTML(directory)
      html = HTMLDocument.new
      head = html.generateHead(keyword,
                               { 'description' => 'The TaskJuggler Manual',
                                 'keywords' =>
                                 'taskjuggler, project, management' })
      head << @manual.generateStyleSheet

      html.html << BODY.new do
        [
          @manual.generateHTMLHeader,
          generateHTMLNavigationBar,

          DIV.new('style' => 'margin-left:5%; margin-right:5%') do
            [
              generateHTMLKeywordBox,
              generateHTMLSupportLevel,
              generateHTMLDescriptionBox,
              generateHTMLOptionalAttributesBox,
              generateHTMLExampleBox
            ]
          end,
          generateHTMLNavigationBar,
          @manual.generateHTMLFooter
        ]
      end

      if directory
        html.write(directory + "#{keyword}.html")
      else
        puts html.to_s
      end
    end

  private

    def checkReference(pattern)
      if pattern.keyword.nil?
        $stderr.puts "Pattern #{pattern} is undocumented but referenced by " +
                     "#{@keyword}."
        false
      end
      true
    end

    def indent(width, str)
      TextFormatter.new(80, width).indent(str)[width..-1]
    end

    # Generate the navigation bar.
    def generateHTMLNavigationBar
      @manual.generateHTMLNavigationBar(
        @predecessor ? @predecessor.title : nil,
        @predecessor ? "#{@predecessor.keyword}.html" : nil,
        @successor ? @successor.title : nil,
        @successor ? "#{@successor.keyword}.html" : nil)
    end

    # Return a HTML object with a link to the manual page for the keyword.
    def keywordHTMLRef(parent, keyword)
      parent << XMLNamedText.new(keyword.title,
                                 'a', 'href' => "#{keyword.keyword}.html")
    end

    # This function is primarily a wrapper around the RichText constructor. It
    # catches all RichTextScanner processing problems and converts the exception
    # data into an error message.
    def newRichText(text)
      rText = RichText.new(text, [])
      unless (rti = rText.generateIntermediateFormat)
        error('rich_text', "Error in RichText of rule #{@keyword}")
      end
      @references += rti.internalReferences
      rti
    end

    # Utility function to turn a list of keywords into a comma separated list
    # of HTML references to the files of these keywords. All embedded in a
    # table cell element. _list_ is the KeywordDocumentation list. _width_ is
    # the percentage width of the cell.
    def listHTMLAttributes(list, width)
      td = XMLElement.new('td', 'class' => 'descr',
                          'style' => "width:#{width}%")
      first = true
      list.each do |attr|
        if first
          first = false
        else
          td << XMLText.new(', ')
        end
        keywordHTMLRef(td, attr)
      end

      td
    end

    def format(indent, str, width)
      TextFormatter.new(width, indent).format(str)[indent..-1]
    end

    def generateHTMLSupportLevel
      if @pattern.supportLevel != :supported
        [
          P.new do
            newRichText("<fcol:red>#{supportLevelMessage}</fcol>").to_html
          end,
          [ :deprecated, :removed ].include?(@pattern.supportLevel) ?
            (P.new { useInsteadMessage }) : nil
        ]
      else
        nil
      end
    end

    def generateHTMLKeywordBox
      # Box with keyword name.
      P.new do
        TABLE.new('align' => 'center', 'class' => 'table') do
          TR.new('align' => 'left') do
            [
              TD.new({ 'class' => 'tag',
                       'style' => 'width:16%'}) { 'Keyword' },
              TD.new({ 'class' => 'descr', 'style' => 'width:84%' }) do
                [
                  B.new() { title },
                  listAttribute? ? A.new({ 'href' => "List_Attributes.html" }) {
                                     "List Attribute" } : ""
                ]
              end
            ]
          end
        end
      end
    end

    def generateHTMLDescriptionBox
      return nil if @pattern.supportLevel == :removed

      # Box with purpose, syntax, arguments and context.
      P.new do
        TABLE.new({ 'align' => 'center', 'class' => 'table' }) do
          [
            COLGROUP.new do
              [
                COL.new('width' => '16%'),
                COL.new('width' => '24%'),
                COL.new('width' => '60%')
              ]
            end,
            generateHTMLPurposeLine,
            generateHTMLSyntaxLine,
            generateHTMLArgumentsLine,
            generateHTMLContextLine,
            generateHTMLAlsoLine
          ]
        end
      end
    end

    def generateHTMLPurposeLine
      generateHTMLTableLine('Purpose', newRichText(@pattern.doc).to_html)
    end

    def generateHTMLSyntaxLine
      if @syntax != '[{ <attributes> }]'
        generateHTMLTableLine('Syntax', CODE.new { @syntax })
      end
    end

    def generateHTMLArgumentsLine
      return nil unless @syntax != '[{ <attributes> }]'

      if @args.empty?
        generateHTMLTableLine('Arguments', 'none')
      else
        rows = []
        first = true
        @args.each do |arg|
          if first
            col1 = 'Arguments'
            col1rows = @args.length
            first = false
          else
            col1 = col1rows = nil
          end
          if arg.typeSpec.nil? || ('<' + arg.name + '>') == arg.typeSpec
            col2 = "#{arg.name}"
          else
            typeSpec = arg.typeSpec
            typeName = typeSpec[1..-2]
            typeSpec[0] = '['
            typeSpec[-1] = ']'
            col2 = [
              "#{arg.name} [",
              A.new('href' =>
                    "The_TaskJuggler_Syntax.html" +
                    "\##{typeName}") { typeName },
              ']'
            ]
          end
          col3 = newRichText(arg.text ||
                             "See [[#{arg.name}]] for details.").to_html
          rows << generateHTMLTableLine(col1, col2, col3, col1rows)
        end
        rows
      end
    end

    def generateHTMLContextLine
      descr = []
      @contexts.each do |c|
        next if [ :deprecated, :removed ].include?(c.pattern.supportLevel)

        descr << ', ' unless descr.empty?
        descr << A.new('href' => "#{c.keyword}.html") { c.title }
      end
      if descr.empty?
        descr = A.new('href' =>
                      'Getting_Started.html#Structure_of_a_TJP_File') do
                        'Global scope'
                      end
      end
      generateHTMLTableLine('Context', descr)
    end

    def generateHTMLAlsoLine
      unless @seeAlso.empty?
        descr = []
        @seeAlso.each do |a|
          next if [ :deprecated, :removed ].include?(a.pattern.supportLevel)

          descr << ', ' unless descr.empty?
          descr << A.new('href' => "#{a.keyword}.html") { a.title }
        end
        generateHTMLTableLine('See also', descr)
      end
    end

    def generateHTMLTableLine(col1, col2, col3 = nil, col1rows = nil)
      return nil if @pattern.supportLevel == :removed

      TR.new('align' => 'left') do
        columns = []
        attrs = { 'class' => 'tag' }
        attrs['rowspan'] = col1rows.to_s if col1rows
        columns << TD.new(attrs) { col1 } if col1
        attrs = { 'class' => 'descr' }
        attrs['colspan'] = '2' unless col3
        columns << TD.new(attrs) { col2 }
        columns << TD.new('class' => 'descr') { col3 } if col3
        columns
      end
    end

    def generateHTMLOptionalAttributesBox
      return nil if @pattern.supportLevel == :removed

      # Box with attributes.
      unless @optionalAttributes.empty?
        @optionalAttributes.sort! do |a, b|
          a.keyword <=> b.keyword
        end

        showDetails = false
        @optionalAttributes.each do |attr|
          if attr.scenarioSpecific || attr.inheritedFromProject ||
             attr.inheritedFromParent
            showDetails = true
            break
          end
        end

        P.new do
          TABLE.new('align' => 'center', 'class' => 'table') do
            if showDetails
              # Table of all attributes with checkmarks for being scenario
              # specific, inherited from parent and inherited from global
              # scope.
              rows = []
              rows << COLGROUP.new do
                [ 16, 24, 20, 20, 20 ].map { |p| COL.new('width' => "#{p}%") }
              end
              rows <<  TR.new('align' => 'left') do
                  [
                    TD.new('class' => 'tag',
                           'rowspan' => "#{@optionalAttributes.length + 1}") do
                      'Attributes'
                    end,
                    TD.new('class' => 'tag') { 'Name' },
                    TD.new('class' => 'tag') { 'Scen. spec.' },
                    TD.new('class' => 'tag') { 'Inh. fm. Global' },
                    TD.new('class' => 'tag') { 'Inh. fm. Parent' }
                  ]
              end

              @optionalAttributes.each do |attr|
                if [ :deprecated, :removed ].include?(attr.pattern.supportLevel)
                  next
                end

                rows << TR.new('align' => 'left') do
                  [
                    TD.new('align' => 'left', 'class' => 'descr') do
                      A.new('href' => "#{attr.keyword}.html") { attr.title }
                    end,
                    TD.new('align' => 'center', 'class' => 'descr') do
                      'x' if attr.scenarioSpecific
                    end,
                    TD.new('align' => 'center', 'class' => 'descr') do
                      'x' if attr.inheritedFromProject
                    end,
                    TD.new('align' => 'center', 'class' => 'descr') do
                      'x' if attr.inheritedFromParent
                    end
                  ]
                end
              end
              rows
            else
              # Comma separated list of all attributes.
              TR.new('align' => 'left') do
                [
                  TD.new('class' => 'tag', 'style' => 'width:16%') do
                    'Attributes'
                  end,
                  TD.new('class' => 'descr', 'style' => 'width:84%') do
                    list = []
                    @optionalAttributes.each do |attr|
                      if [ :deprecated, :removed ].
                         include?(attr.pattern.supportLevel)
                        next
                      end

                      list << ', ' unless list.empty?
                      list << A.new('href' => "#{attr.keyword}.html") do
                        attr.title
                      end
                    end
                    list
                  end
                ]
              end
            end
          end
        end
      end
    end

    def generateHTMLExampleBox
      return nil if @pattern.supportLevel == :removed

      if @pattern.exampleFile
        exampleDir = File.join(AppConfig.dataDirs('test')[0], 'TestSuite',
                               'Syntax', 'Correct')
        example = TjpExample.new
        fileName = "#{exampleDir}/#{@pattern.exampleFile}.tjp"
        example.open(fileName)
        unless (text = example.to_s(@pattern.exampleTag))
          raise "There is no tag '#{@pattern.exampleTag}' in file " +
            "#{fileName}."
        end

        DIV.new('class' => 'codeframe') do
          PRE.new('class' => 'code') { text }
        end
      end
    end

    def supportLevelMessage
      case @pattern.supportLevel
      when :experimental
        "This keyword is currently in an experimental state. " +
        "The implementation is probably still incomplete and " +
        "use of this keyword may lead to wrong results. Do not " +
        "use this keyword unless you were specifically directed " +
        "by the developers to try it."
      when :beta
        "This keyword has not yet been fully tested yet. You are " +
        "welcome to try it, but it may lead to wrong results. " +
        "The syntax may still change with future versions. " +
        "The developers appreciate any feedback on this keyword."
      when :deprecated
        "This keyword should no longer be used. It will be removed " +
        "in future versions of this software."
      when :removed
        "This keyword is no longer supported."
      end
    end

    def useInsteadMessage
      return nil if @seeAlso.empty?

      descr = [ 'Use ' ]
      @seeAlso.each do |a|
        descr << ', ' unless descr.length <= 1
        descr << A.new('href' => "#{a.keyword}.html") { a.title }
      end
      descr << " instead."
    end

    def findPropertySet
      property = nil
      @contexts.each do |kwd|
        if %w( task resource account shift scenario
               accountreport resourcereport taskreport textreport ).
               include?(kwd.keyword)
          property = kwd.keyword
          break
        end
      end
      if property
        project = Project.new('id', 'dummy', '1.0')
        case property
        when 'task'
          project.tasks
        when 'resource'
          project.resources
        when 'account'
          project.accounts
        when 'shift'
          project.shifts
        when 'scenario'
          project.scenarios
        else
          project.reports
        end
      else
        nil
      end
    end

  end

end