Almirah 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/bin/almirah +2 -2
  3. data/lib/almirah/doc_fabric.rb +8 -0
  4. data/lib/almirah/doc_items/blockquote.rb +15 -16
  5. data/lib/almirah/doc_items/code_block.rb +19 -21
  6. data/lib/almirah/doc_items/controlled_paragraph.rb +2 -2
  7. data/lib/almirah/doc_items/controlled_table.rb +11 -11
  8. data/lib/almirah/doc_items/controlled_table_row.rb +15 -18
  9. data/lib/almirah/doc_items/doc_footer.rb +10 -13
  10. data/lib/almirah/doc_items/doc_item.rb +2 -2
  11. data/lib/almirah/doc_items/heading.rb +21 -3
  12. data/lib/almirah/doc_items/image.rb +25 -20
  13. data/lib/almirah/doc_items/markdown_list.rb +2 -2
  14. data/lib/almirah/doc_items/markdown_table.rb +10 -3
  15. data/lib/almirah/doc_items/scope_table.rb +188 -0
  16. data/lib/almirah/doc_items/text_line.rb +52 -27
  17. data/lib/almirah/doc_items/todo_block.rb +15 -16
  18. data/lib/almirah/doc_items/work_item.rb +129 -0
  19. data/lib/almirah/doc_parser.rb +14 -8
  20. data/lib/almirah/doc_types/base_document.rb +21 -5
  21. data/lib/almirah/doc_types/coverage.rb +3 -3
  22. data/lib/almirah/doc_types/critical_chain_page.rb +218 -0
  23. data/lib/almirah/doc_types/decision.rb +158 -7
  24. data/lib/almirah/doc_types/decision_grouping.rb +17 -0
  25. data/lib/almirah/doc_types/decisions_overview.rb +593 -32
  26. data/lib/almirah/doc_types/implementation.rb +98 -98
  27. data/lib/almirah/doc_types/index.rb +11 -12
  28. data/lib/almirah/doc_types/persistent_document.rb +1 -1
  29. data/lib/almirah/doc_types/planning_dates.rb +17 -0
  30. data/lib/almirah/doc_types/protocol.rb +16 -20
  31. data/lib/almirah/doc_types/source_file.rb +1 -1
  32. data/lib/almirah/doc_types/traceability.rb +124 -133
  33. data/lib/almirah/dom/doc_section.rb +2 -2
  34. data/lib/almirah/dom/document.rb +1 -1
  35. data/lib/almirah/html_safe.rb +39 -0
  36. data/lib/almirah/link_registry.rb +21 -6
  37. data/lib/almirah/navigation_pane.rb +9 -13
  38. data/lib/almirah/project/critical_chain.rb +117 -0
  39. data/lib/almirah/project/doc_linker.rb +4 -4
  40. data/lib/almirah/project/fever_chart.rb +94 -0
  41. data/lib/almirah/project/project_data.rb +8 -2
  42. data/lib/almirah/project/work_item_scheduler.rb +166 -0
  43. data/lib/almirah/project/working_calendar.rb +112 -0
  44. data/lib/almirah/project.rb +146 -9
  45. data/lib/almirah/project_configuration.rb +126 -29
  46. data/lib/almirah/project_template.rb +6 -6
  47. data/lib/almirah/project_utility.rb +3 -5
  48. data/lib/almirah/search/specifications_db.rb +2 -2
  49. data/lib/almirah/source_file_parser.rb +2 -3
  50. data/lib/almirah/templates/css/main.css +168 -1
  51. data/lib/almirah/templates/scripts/main.js +7 -2
  52. data/lib/almirah/templates/scripts/orama_search.js +17 -4
  53. data/lib/almirah.rb +1 -2
  54. metadata +12 -2
@@ -1,6 +1,7 @@
1
1
  require 'cgi'
2
2
  require 'uri'
3
3
  require_relative '../relative_url'
4
+ require_relative '../html_safe'
4
5
 
5
6
  class TextLineToken
6
7
  attr_accessor :value
@@ -11,25 +12,25 @@ class TextLineToken
11
12
  end
12
13
 
13
14
  class ItalicToken < TextLineToken
14
- def initialize # rubocop:disable Lint/MissingSuper
15
+ def initialize
15
16
  @value = '*'
16
17
  end
17
18
  end
18
19
 
19
20
  class BoldToken < TextLineToken
20
- def initialize # rubocop:disable Lint/MissingSuper
21
+ def initialize
21
22
  @value = '**'
22
23
  end
23
24
  end
24
25
 
25
26
  class BoldAndItalicToken < TextLineToken
26
- def initialize # rubocop:disable Lint/MissingSuper
27
+ def initialize
27
28
  @value = '***'
28
29
  end
29
30
  end
30
31
 
31
32
  class ParentheseLeft < TextLineToken
32
- def initialize # rubocop:disable Lint/MissingSuper
33
+ def initialize
33
34
  @value = '('
34
35
  end
35
36
  end
@@ -41,7 +42,7 @@ class ParentheseRight < TextLineToken
41
42
  end
42
43
 
43
44
  class SquareBracketLeft < TextLineToken
44
- def initialize # rubocop:disable Lint/MissingSuper
45
+ def initialize
45
46
  @value = '['
46
47
  end
47
48
  end
@@ -53,13 +54,13 @@ class SquareBracketRight < TextLineToken
53
54
  end
54
55
 
55
56
  class DoubleSquareBracketLeft < TextLineToken
56
- def initialize # rubocop:disable Lint/MissingSuper
57
+ def initialize
57
58
  @value = '[['
58
59
  end
59
60
  end
60
61
 
61
62
  class DoubleSquareBracketRight < TextLineToken
62
- def initialize # rubocop:disable Lint/MissingSuper
63
+ def initialize
63
64
  @value = ']]'
64
65
  end
65
66
  end
@@ -71,13 +72,13 @@ class SquareBracketRightAndParentheseLeft < TextLineToken
71
72
  end
72
73
 
73
74
  class BacktickToken < TextLineToken
74
- def initialize # rubocop:disable Lint/MissingSuper
75
+ def initialize
75
76
  @value = '`'
76
77
  end
77
78
  end
78
79
 
79
80
  class InlineCodeToken < TextLineToken
80
- def initialize(raw) # rubocop:disable Lint/MissingSuper
81
+ def initialize(raw)
81
82
  @value = raw
82
83
  end
83
84
  end
@@ -85,7 +86,7 @@ end
85
86
  class TextLineParser
86
87
  attr_accessor :supported_tokens
87
88
 
88
- def initialize # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
89
+ def initialize
89
90
  @supported_tokens = []
90
91
  @supported_tokens.append(BoldAndItalicToken.new)
91
92
  @supported_tokens.append(BoldToken.new)
@@ -101,7 +102,7 @@ class TextLineParser
101
102
  @supported_tokens.append(TextLineToken.new)
102
103
  end
103
104
 
104
- def tokenize(str) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
105
+ def tokenize(str)
105
106
  result = []
106
107
  sl = str.length
107
108
  si = 0
@@ -133,7 +134,7 @@ class TextLineParser
133
134
 
134
135
  private
135
136
 
136
- def fuse_backticks(tokens) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
137
+ def fuse_backticks(tokens)
137
138
  result = []
138
139
  i = 0
139
140
  while i < tokens.length
@@ -217,6 +218,12 @@ class TextLineParser
217
218
  end
218
219
 
219
220
  class TextLineBuilderContext
221
+ # Literal (non-markup) text run. Subclasses encode it for the HTML context;
222
+ # the base context leaves it untouched for plain reconstruction/unit tests.
223
+ def literal_text(str)
224
+ str
225
+ end
226
+
220
227
  def italic(str)
221
228
  str
222
229
  end
@@ -249,7 +256,7 @@ class TextLineBuilder
249
256
  @builder_context = builder_context
250
257
  end
251
258
 
252
- def restore(token_list) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
259
+ def restore(token_list)
253
260
  result = ''
254
261
  return '' if token_list.nil?
255
262
 
@@ -363,7 +370,11 @@ class TextLineBuilder
363
370
  tii += 1
364
371
  end
365
372
  if is_found
366
- result += @builder_context.link(restore(sub_list_url_text), restore(sub_list_url_address))
373
+ # URL is reconstructed raw (not via restore) so scheme classification
374
+ # and file-path resolution see the original characters; link() applies
375
+ # attribute escaping and the scheme allow-list (ADR-188).
376
+ raw_url = (sub_list_url_address || []).map(&:value).join
377
+ result += @builder_context.link(restore(sub_list_url_text), raw_url)
367
378
  else
368
379
  result += '['
369
380
  ti = ti_starting_position + 1
@@ -373,7 +384,7 @@ class TextLineBuilder
373
384
  result += @builder_context.inline_code(token_list[ti].value)
374
385
  ti += 1
375
386
  when 'TextLineToken', 'ParentheseLeft', 'ParentheseRight', 'SquareBracketRight', 'DoubleSquareBracketRight'
376
- result += token_list[ti].value
387
+ result += @builder_context.literal_text(token_list[ti].value)
377
388
  ti += 1
378
389
  else
379
390
  ti += 1
@@ -384,12 +395,14 @@ class TextLineBuilder
384
395
  end
385
396
 
386
397
  class TextLine < TextLineBuilderContext
387
- @@link_registry = nil # rubocop:disable Style/ClassVars
388
- @@broken_links = [] # rubocop:disable Style/ClassVars
398
+ include HtmlSafe
399
+
400
+ @@link_registry = nil
401
+ @@broken_links = []
389
402
 
390
403
  class << self
391
404
  def link_registry=(registry)
392
- @@link_registry = registry # rubocop:disable Style/ClassVars
405
+ @@link_registry = registry
393
406
  end
394
407
 
395
408
  def link_registry
@@ -403,7 +416,7 @@ class TextLine < TextLineBuilderContext
403
416
  end
404
417
 
405
418
  def reset_broken_links
406
- @@broken_links = [] # rubocop:disable Style/ClassVars
419
+ @@broken_links = []
407
420
  end
408
421
 
409
422
  def record_broken_link(document, target)
@@ -423,6 +436,11 @@ class TextLine < TextLineBuilderContext
423
436
  tlb.restore(tlp.tokenize(str))
424
437
  end
425
438
 
439
+ # Literal text run, HTML-escaped for element content (ADR-188, SRS-096).
440
+ def literal_text(str)
441
+ escape_text(str)
442
+ end
443
+
426
444
  def italic(str)
427
445
  "<i>#{str}</i>"
428
446
  end
@@ -439,24 +457,27 @@ class TextLine < TextLineBuilderContext
439
457
  "<code class=\"inline\">#{CGI.escapeHTML(str)}</code>"
440
458
  end
441
459
 
442
- def link(link_text, link_url) # rubocop:disable Metrics/MethodLength
460
+ def link(link_text, link_url)
443
461
  raw = link_url.to_s
444
462
  kind, target, fragment = classify_markdown_link(raw)
445
463
  case kind
446
464
  when :internal
447
465
  href = RelativeUrl.between(owner_document.output_rel_path, target.output_rel_path, fragment: fragment)
448
- "<a href=\"#{href}\" class=\"external\">#{link_text}</a>"
466
+ "<a href=\"#{escape_attr(href)}\" class=\"external\">#{link_text}</a>"
449
467
  when :broken
450
468
  TextLine.record_broken_link(owner_document, raw)
451
- "<a href=\"#{raw}\" class=\"broken_link\" title=\"Unresolved cross-document link\">#{link_text}</a>"
469
+ "<a href=\"#{escape_attr(raw)}\" class=\"broken_link\" title=\"Unresolved cross-document link\">#{link_text}</a>"
452
470
  else
453
- "<a target=\"_blank\" rel=\"noopener\" href=\"#{raw}\" class=\"external\">#{link_text}</a>"
471
+ url = safe_url(raw)
472
+ return link_text if url.nil? # disallowed scheme: render inert (ADR-188, SRS-098)
473
+
474
+ "<a target=\"_blank\" rel=\"noopener\" href=\"#{escape_attr(url)}\" class=\"external\">#{link_text}</a>"
454
475
  end
455
476
  end
456
477
 
457
478
  # Resolves an Obsidian/wiki link "[[target#fragment|alias]]" to a managed
458
479
  # document by its unique id/filename, independent of folder (ADR-186).
459
- def wiki_link(inner) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize,Metrics/MethodLength
480
+ def wiki_link(inner)
460
481
  link_part, sep, alias_text = inner.partition('|')
461
482
  target, _hash, fragment = link_part.partition('#')
462
483
  display = (sep.empty? ? link_part : alias_text).strip
@@ -480,7 +501,7 @@ class TextLine < TextLineBuilderContext
480
501
  # Classifies a Markdown link target as :internal (a managed document, resolved
481
502
  # against the owning document's source directory), :broken (a local .md path
482
503
  # that does not resolve), or :external (everything else). ADR-186.
483
- def classify_markdown_link(raw) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
504
+ def classify_markdown_link(raw)
484
505
  path_part, _sep, fragment = raw.partition('#')
485
506
  path_part = URI::DEFAULT_PARSER.unescape(path_part) # decode %20 etc. to match the real file path
486
507
  return [:external] unless local_markdown_path?(path_part)
@@ -489,10 +510,14 @@ class TextLine < TextLineBuilderContext
489
510
  return [:external] unless doc&.output_rel_path && doc.respond_to?(:path) && doc.path && TextLine.link_registry
490
511
 
491
512
  target = TextLine.link_registry.find_by_source(File.expand_path(path_part, File.dirname(doc.path)))
492
- target ? [:internal, target, fragment.empty? ? nil : fragment] : [:broken]
513
+ if target
514
+ [:internal, target, fragment.empty? ? nil : fragment]
515
+ else
516
+ [:broken]
517
+ end
493
518
  end
494
519
 
495
520
  def local_markdown_path?(path_part)
496
- path_part.match?(/\.(md|markdown)\z/i) && !path_part.match?(%r{\A[a-z][a-z0-9+.\-]*://}i)
521
+ path_part.match?(/\.(md|markdown)\z/i) && !path_part.match?(%r{\A[a-z][a-z0-9+.-]*://}i)
497
522
  end
498
523
  end
@@ -1,22 +1,21 @@
1
- require_relative "doc_item"
1
+ require_relative 'doc_item'
2
2
 
3
3
  class TodoBlock < DocItem
4
+ attr_accessor :text
4
5
 
5
- attr_accessor :text
6
+ def initialize(text)
7
+ @text = text
8
+ end
6
9
 
7
- def initialize(text)
8
- @text = text
10
+ def to_html
11
+ s = ''
12
+ f_text = format_string(@text)
13
+ if @@html_table_render_in_progress
14
+ s += "</table>\n"
15
+ @@html_table_render_in_progress = false
9
16
  end
10
17
 
11
- def to_html
12
- s = ''
13
- f_text = format_string(@text)
14
- if @@html_table_render_in_progress
15
- s += "</table>\n"
16
- @@html_table_render_in_progress = false
17
- end
18
-
19
- s += "<div class=\"todoblock\"><p>#{f_text}</div>\n"
20
- return s
21
- end
22
- end
18
+ s += "<div class=\"todoblock\"><p>#{f_text}</div>\n"
19
+ s
20
+ end
21
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A single Scope-table row of a Decision Record, modelled as a node in the
4
+ # per-row dependency network (ADR-194). Each work item carries its canonical
5
+ # identity (`<record>.<step>.<activity>`), the bounded per-row Status it reads
6
+ # its readiness from (ADR-193), and predecessor / successor edges spanning both
7
+ # intra-record (step order) and cross-record (Depends On) links.
8
+ #
9
+ # `predecessors` / `successors` follow the house list-of-single-key-hashes shape
10
+ # (ADR-197): each entry maps a readable `record.step.activity` label to the
11
+ # resolved WorkItem. Every cross-record edge additionally records whether it
12
+ # stays within a decision_groups folder (in-group) or crosses to another
13
+ # (cross-group); that tag is inert here and consumed by the critical-chain step
14
+ # (ADR-195). Readiness ignores it — a cross-group predecessor blocks exactly
15
+ # like an in-group one.
16
+ class WorkItem
17
+ # Canonical phase order used only as the fallback for activity-type-aligned
18
+ # resolution when the prerequisite has no row of the dependent's exact Item.
19
+ ACTIVITY_ORDER = %w[Analysis Requirements Code Tests].freeze
20
+
21
+ attr_reader :record_id, :step, :activity, :owner, :status, :depends_on_refs,
22
+ :focused_estimate, :safe_estimate, :record_sequence, :start_date, :target_date
23
+ attr_accessor :predecessors, :successors, :cross_group_predecessor_labels, :resolved_dependencies
24
+
25
+ def initialize(record_id:, step:, activity:, owner:, status:, depends_on_refs:, # rubocop:disable Metrics/ParameterLists
26
+ focused_estimate: 0.0, safe_estimate: 0.0, record_sequence: 0,
27
+ start_date: nil, target_date: nil)
28
+ @record_id = record_id
29
+ @step = step
30
+ @activity = activity.to_s.strip
31
+ @owner = owner.to_s.strip
32
+ @status = status.to_s.strip
33
+ @depends_on_refs = depends_on_refs # array of record ref strings, e.g. ["ADR-193"]
34
+ # The row's authored committed window (ADR-213): the Scope Start Date and
35
+ # Target Date as parsed Dates (nil when absent/unparseable). Distinct from the
36
+ # computed schedule -- these are the author's intent, drawn on the tracking lane.
37
+ @start_date = start_date
38
+ @target_date = target_date
39
+ # Per-row scheduling estimates in working days (ADR-195); the focused estimate
40
+ # is the scheduling duration, safe - focused the safety aggregated into the
41
+ # project buffer. record_sequence is the owning record's number, used only as
42
+ # a deterministic priority tiebreak in the critical-chain scheduler.
43
+ @focused_estimate = focused_estimate
44
+ @safe_estimate = safe_estimate
45
+ @record_sequence = record_sequence
46
+ @predecessors = []
47
+ @successors = []
48
+ @cross_group_predecessor_labels = []
49
+ # One entry per resolved Depends On reference, keyed by the authored ref:
50
+ # { ref => { doc:, anchor:, label: } }. `anchor` is the target's resolved
51
+ # work-item row anchor (nil when the target has no `#` step column, so the
52
+ # link opens the record page); `label` is the resolved work item's id, for
53
+ # the link tooltip. Used to render the Depends On cell as deep links.
54
+ @resolved_dependencies = {}
55
+ end
56
+
57
+ def id
58
+ "#{@record_id}.#{@step}.#{@activity}"
59
+ end
60
+
61
+ # The anchor of this work item's row in its rendered Scope table — the deep-link
62
+ # target a dependent record points at. Namespaced with `.scope.` so it never
63
+ # collides with the Affected Documents controlled table, whose rows anchor on
64
+ # the same `<record>.<step>` scheme on the same page.
65
+ def row_anchor
66
+ "#{@record_id}.scope.#{@step}"
67
+ end
68
+
69
+ def add_resolved_dependency(ref, doc, anchor, label)
70
+ @resolved_dependencies[ref] = { doc: doc, anchor: anchor, label: label }
71
+ end
72
+
73
+ def activity_rank
74
+ ACTIVITY_ORDER.index(@activity) || ACTIVITY_ORDER.length
75
+ end
76
+
77
+ # A row is "started" once it leaves To Do; both In-Progress and Done count, so
78
+ # a Done row whose predecessor never finished is still flagged as a violation.
79
+ def started?
80
+ @status == 'In-Progress' || @status == 'Done'
81
+ end
82
+
83
+ def done?
84
+ @status == 'Done'
85
+ end
86
+
87
+ def add_predecessor(work_item, cross_group:)
88
+ @predecessors << { work_item.id => work_item }
89
+ @cross_group_predecessor_labels << work_item.id if cross_group
90
+ end
91
+
92
+ def add_successor(work_item)
93
+ @successors << { work_item.id => work_item }
94
+ end
95
+
96
+ def predecessor_items
97
+ @predecessors.map { |edge| edge.values.first }
98
+ end
99
+
100
+ def successor_items
101
+ @successors.map { |edge| edge.values.first }
102
+ end
103
+
104
+ # Same-record, lower-numbered steps — the intra-record (phase-order) edges.
105
+ def intra_record_predecessors
106
+ predecessor_items.select { |p| p.record_id == @record_id }
107
+ end
108
+
109
+ # Resolved Depends On edges into other records (in-group or cross-group alike).
110
+ def cross_record_predecessors
111
+ predecessor_items.reject { |p| p.record_id == @record_id }
112
+ end
113
+
114
+ # Kitted when every predecessor — intra- and cross-record, regardless of the
115
+ # in-group / cross-group tag — is Done; trivially kitted when it has none.
116
+ def fully_kitted?
117
+ predecessor_items.all?(&:done?)
118
+ end
119
+
120
+ # A started row with an unfinished lower-numbered step in the same record.
121
+ def phase_order_violation?
122
+ started? && intra_record_predecessors.any? { |p| !p.done? }
123
+ end
124
+
125
+ # A started row whose resolved cross-record predecessor is not yet Done.
126
+ def cross_record_violation?
127
+ started? && cross_record_predecessors.any? { |p| !p.done? }
128
+ end
129
+ end
@@ -8,13 +8,14 @@ require_relative 'doc_items/todo_block'
8
8
  require_relative 'doc_items/controlled_paragraph'
9
9
  require_relative 'doc_items/markdown_table'
10
10
  require_relative 'doc_items/controlled_table'
11
+ require_relative 'doc_items/scope_table'
11
12
  require_relative 'doc_items/image'
12
13
  require_relative 'doc_items/markdown_list'
13
14
  require_relative 'doc_items/doc_footer'
14
15
  require_relative 'doc_items/frontmatter'
15
16
 
16
- class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
17
- def self.try_to_extract_frontmatter(doc, text_lines) # rubocop:disable Metrics/MethodLength
17
+ class DocParser
18
+ def self.try_to_extract_frontmatter(doc, text_lines)
18
19
  lines_to_remove = 0
19
20
  frontmatter_lines = ''
20
21
  if /^(-{3,})/.match(text_lines[0])
@@ -174,7 +175,7 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
174
175
 
175
176
  doc.items.append(item)
176
177
 
177
- elsif res = /^([*\-]\s+)(.*)/.match(s) # check if unordered list start
178
+ elsif res = /^([*-]\s+)(.*)/.match(s) # check if unordered list start
178
179
 
179
180
  if doc.title == ''
180
181
  # dummy section if root is not a Document Title (level 0)
@@ -187,7 +188,7 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
187
188
 
188
189
  temp_md_table = process_temp_table(doc, temp_md_table)
189
190
 
190
- row = res[2]
191
+ res[2]
191
192
 
192
193
  if temp_md_list
193
194
  temp_md_list.add_row(s)
@@ -201,7 +202,7 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
201
202
 
202
203
  temp_md_table = process_temp_table(doc, temp_md_table)
203
204
 
204
- row = res[1]
205
+ res[1]
205
206
 
206
207
  if temp_md_list
207
208
  temp_md_list.add_row(s)
@@ -248,9 +249,14 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
248
249
 
249
250
  if temp_md_table
250
251
  if temp_md_table.is_separator_detected # if there is a separator
251
- # check if parent doc is a Protocol, or a Decision Record inside an "Affected Documents" section
252
- if doc.instance_of?(Protocol) ||
253
- (doc.instance_of?(Decision) && in_section?(doc, 'Affected Documents'))
252
+ # A Decision Record's Scope table is parsed into a purpose-built
253
+ # ScopeTable (ADR-194); a Protocol, or a Decision inside an
254
+ # "Affected Documents" section, into a ControlledTable.
255
+ if doc.instance_of?(Decision) && in_section?(doc, 'Scope') &&
256
+ temp_md_table.instance_of?(MarkdownTable)
257
+ temp_md_table = ScopeTable.new(doc, temp_md_table)
258
+ elsif doc.instance_of?(Protocol) ||
259
+ (doc.instance_of?(Decision) && in_section?(doc, 'Affected Documents'))
254
260
  # check if it is a controlled table
255
261
  tmp = /(.*)\s+>\[(\S*)\]/.match(row)
256
262
  if tmp && (temp_md_table.instance_of? MarkdownTable)
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative '../relative_url'
4
4
 
5
- class BaseDocument # rubocop:disable Style/Documentation
5
+ class BaseDocument
6
6
  attr_accessor :title, :id, :dom, :headings, :output_rel_path
7
7
 
8
8
  class << self
@@ -17,7 +17,13 @@ class BaseDocument # rubocop:disable Style/Documentation
17
17
  @dom = nil
18
18
  end
19
19
 
20
- def save_html_to_file(html_rows, nav_pane, output_file_path) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
20
+ # Whether this page needs the Chart.js library loaded (overridden by the
21
+ # planning pages that render charts).
22
+ def needs_chartjs?
23
+ false
24
+ end
25
+
26
+ def save_html_to_file(html_rows, nav_pane, output_file_path)
21
27
  gem_root = File.expand_path './../../..', File.dirname(__FILE__)
22
28
  template_file = "#{gem_root}/lib/almirah/templates/page.html"
23
29
 
@@ -29,6 +35,8 @@ class BaseDocument # rubocop:disable Style/Documentation
29
35
  "#{@id}.html"
30
36
  elsif instance_of? DecisionsOverview
31
37
  'overview.html'
38
+ elsif instance_of? CriticalChainPage
39
+ 'critical-chain.html'
32
40
  elsif instance_of? Decision
33
41
  "#{@id}.html"
34
42
  else
@@ -36,7 +44,7 @@ class BaseDocument # rubocop:disable Style/Documentation
36
44
  end
37
45
  @output_rel_path = output_file_path.split('/build/', 2).last
38
46
  file = File.open(output_file_path, 'w')
39
- file_data.each do |s| # rubocop:disable Metrics/BlockLength
47
+ file_data.each do |s|
40
48
  if s.include?('{{CONTENT}}')
41
49
  html_rows.each do |r|
42
50
  file.puts r
@@ -51,7 +59,7 @@ class BaseDocument # rubocop:disable Style/Documentation
51
59
  if @id == 'index'
52
60
  file.puts "<script type=\"module\" src=\"#{rel_to('scripts/orama_search.js')}\"></script>"
53
61
  file.puts "<link rel=\"stylesheet\" href=\"#{rel_to('css/search.css')}\">"
54
- elsif instance_of? DecisionsOverview
62
+ elsif needs_chartjs?
55
63
  file.puts '<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>'
56
64
  end
57
65
  elsif s.include?('{{HOME_BUTTON}}')
@@ -60,7 +68,10 @@ class BaseDocument # rubocop:disable Style/Documentation
60
68
  else
61
69
  file.puts index_link(rel_to('index.html'))
62
70
  end
63
- file.puts decisions_link(rel_to('decisions/overview.html')) if BaseDocument.show_decisions_link
71
+ if BaseDocument.show_decisions_link
72
+ file.puts decisions_link(rel_to('decisions/overview.html'))
73
+ file.puts critical_chain_link(rel_to('decisions/critical-chain.html'))
74
+ end
64
75
  elsif s.include?('{{GEM_VERSION}}')
65
76
  file.puts "(#{Gem.loaded_specs['Almirah'].version.version})"
66
77
  else
@@ -75,6 +86,11 @@ class BaseDocument # rubocop:disable Style/Documentation
75
86
  %(<a id="decisions_menu_item" href="#{href}">#{icon}&nbsp;Decision Records</a>)
76
87
  end
77
88
 
89
+ def critical_chain_link(href)
90
+ icon = '<span><i class="fa fa-link" aria-hidden="true"></i></span>'
91
+ %(<a id="critical_chain_menu_item" href="#{href}">#{icon}&nbsp;Critical Chain</a>)
92
+ end
93
+
78
94
  def index_link(href)
79
95
  icon = '<span><i class="fa fa-info" aria-hidden="true"></i></span>'
80
96
  %(<a id="index_menu_item" href="#{href}">#{icon}&nbsp;Index</a>)
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative 'base_document'
4
4
 
5
- class Coverage < BaseDocument # rubocop:disable Style/Documentation
5
+ class Coverage < BaseDocument
6
6
  attr_accessor :top_doc, :bottom_doc, :covered_items, :passed_steps_number, :failed_steps_number
7
7
 
8
8
  def initialize(top_doc)
@@ -21,7 +21,7 @@ class Coverage < BaseDocument # rubocop:disable Style/Documentation
21
21
  puts "\e[35mTraceability: #{@id}\e[0m"
22
22
  end
23
23
 
24
- def to_html(nav_pane, output_file_path) # rubocop:disable Metrics/MethodLength
24
+ def to_html(nav_pane, output_file_path)
25
25
  html_rows = []
26
26
 
27
27
  html_rows.append('')
@@ -43,7 +43,7 @@ class Coverage < BaseDocument # rubocop:disable Style/Documentation
43
43
  save_html_to_file(html_rows, nav_pane, output_file_path)
44
44
  end
45
45
 
46
- def render_table_row(top_item) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
46
+ def render_table_row(top_item)
47
47
  s = ''
48
48
  if top_item.coverage_links
49
49
  id_color = if top_item.coverage_links.length > 1