Almirah 0.4.2 → 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.
- checksums.yaml +4 -4
- data/bin/almirah +2 -2
- data/lib/almirah/doc_fabric.rb +7 -0
- data/lib/almirah/doc_items/blockquote.rb +15 -16
- data/lib/almirah/doc_items/code_block.rb +19 -21
- data/lib/almirah/doc_items/controlled_paragraph.rb +2 -2
- data/lib/almirah/doc_items/controlled_table.rb +11 -11
- data/lib/almirah/doc_items/controlled_table_row.rb +15 -18
- data/lib/almirah/doc_items/doc_footer.rb +10 -13
- data/lib/almirah/doc_items/doc_item.rb +2 -2
- data/lib/almirah/doc_items/heading.rb +1 -1
- data/lib/almirah/doc_items/image.rb +25 -27
- data/lib/almirah/doc_items/markdown_list.rb +2 -2
- data/lib/almirah/doc_items/markdown_table.rb +9 -2
- data/lib/almirah/doc_items/scope_table.rb +188 -0
- data/lib/almirah/doc_items/text_line.rb +26 -22
- data/lib/almirah/doc_items/todo_block.rb +15 -16
- data/lib/almirah/doc_items/work_item.rb +129 -0
- data/lib/almirah/doc_parser.rb +14 -8
- data/lib/almirah/doc_types/base_document.rb +21 -5
- data/lib/almirah/doc_types/coverage.rb +3 -3
- data/lib/almirah/doc_types/critical_chain_page.rb +218 -0
- data/lib/almirah/doc_types/decision.rb +152 -7
- data/lib/almirah/doc_types/decision_grouping.rb +17 -0
- data/lib/almirah/doc_types/decisions_overview.rb +591 -31
- data/lib/almirah/doc_types/implementation.rb +98 -98
- data/lib/almirah/doc_types/index.rb +11 -12
- data/lib/almirah/doc_types/persistent_document.rb +1 -1
- data/lib/almirah/doc_types/planning_dates.rb +17 -0
- data/lib/almirah/doc_types/protocol.rb +16 -20
- data/lib/almirah/doc_types/source_file.rb +1 -1
- data/lib/almirah/doc_types/traceability.rb +124 -133
- data/lib/almirah/dom/doc_section.rb +1 -1
- data/lib/almirah/navigation_pane.rb +9 -13
- data/lib/almirah/project/critical_chain.rb +117 -0
- data/lib/almirah/project/doc_linker.rb +4 -4
- data/lib/almirah/project/fever_chart.rb +94 -0
- data/lib/almirah/project/project_data.rb +8 -2
- data/lib/almirah/project/work_item_scheduler.rb +166 -0
- data/lib/almirah/project/working_calendar.rb +112 -0
- data/lib/almirah/project.rb +146 -9
- data/lib/almirah/project_configuration.rb +126 -29
- data/lib/almirah/project_template.rb +6 -6
- data/lib/almirah/project_utility.rb +3 -5
- data/lib/almirah/search/specifications_db.rb +2 -2
- data/lib/almirah/source_file_parser.rb +2 -3
- data/lib/almirah/templates/css/main.css +160 -0
- data/lib/almirah/templates/scripts/main.js +3 -1
- data/lib/almirah.rb +1 -2
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46e67646a16db801d9dbe0cd2c2873a38e6e35068c5cde855179b9f5beb00089
|
|
4
|
+
data.tar.gz: c4953874698c949e6711ebdfa0c84dc0b56d6291674a7127d6ccfbb31802cea4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1401a30fbde34dd6c21b77685e3e2506753e7b302d528189e5d3d129eff6f9a15dd169f756be53ea1467dc5805fe36d0ce0678d2a446a866b18e95c66e70de64
|
|
7
|
+
data.tar.gz: 1db3dd4230d6b77154758bdbad3802fb121ba23c41a3ee33aeb8ba70ac0be4a7ac0d8e14ea05667869d2625fcc638ca2d900a47270f22ff9bac1077c932173ba
|
data/bin/almirah
CHANGED
data/lib/almirah/doc_fabric.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative 'doc_types/implementation'
|
|
|
7
7
|
require_relative 'doc_types/traceability'
|
|
8
8
|
require_relative 'doc_types/decision'
|
|
9
9
|
require_relative 'doc_types/decisions_overview'
|
|
10
|
+
require_relative 'doc_types/critical_chain_page'
|
|
10
11
|
require_relative 'doc_parser'
|
|
11
12
|
require_relative 'source_file_parser'
|
|
12
13
|
require_relative 'dom/document'
|
|
@@ -67,6 +68,8 @@ class DocFabric
|
|
|
67
68
|
doc.extract_start_date
|
|
68
69
|
doc.extract_target_date
|
|
69
70
|
doc.extract_target_release_version
|
|
71
|
+
doc.extract_owners
|
|
72
|
+
doc.extract_scope_table
|
|
70
73
|
doc
|
|
71
74
|
end
|
|
72
75
|
|
|
@@ -74,6 +77,10 @@ class DocFabric
|
|
|
74
77
|
DecisionsOverview.new project
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
def self.create_critical_chain_page(project)
|
|
81
|
+
CriticalChainPage.new project
|
|
82
|
+
end
|
|
83
|
+
|
|
77
84
|
def self.create_source_file(repository_path, path, repository_name)
|
|
78
85
|
doc = SourceFile.new repository_path, path, repository_name
|
|
79
86
|
DocFabric.parse_source_file doc
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative 'doc_item'
|
|
2
2
|
|
|
3
3
|
class Blockquote < DocItem
|
|
4
|
+
attr_accessor :text
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
def initialize(text)
|
|
7
|
+
@text = text
|
|
8
|
+
end
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
s += "</table>\n"
|
|
16
|
-
@@html_table_render_in_progress = false
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
s += "<div class=\"blockquote\"><p>#{f_text}</div>\n"
|
|
20
|
-
return s
|
|
21
|
-
end
|
|
22
|
-
end
|
|
18
|
+
s += "<div class=\"blockquote\"><p>#{f_text}</div>\n"
|
|
19
|
+
s
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative 'doc_item'
|
|
2
2
|
|
|
3
3
|
class CodeBlock < DocItem
|
|
4
|
+
attr_accessor :suggested_format, :code_lines
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
def initialize(suggested_format)
|
|
7
|
+
@suggested_format = suggested_format
|
|
8
|
+
@code_lines = []
|
|
9
|
+
end
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@code_lines = Array.new
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def to_html
|
|
14
|
-
s = ''
|
|
11
|
+
def to_html
|
|
12
|
+
s = ''
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
end
|
|
24
|
-
s += "</code>\n"
|
|
25
|
-
return s
|
|
14
|
+
if @@html_table_render_in_progress
|
|
15
|
+
s += "</table>\n"
|
|
16
|
+
@@html_table_render_in_progress = false
|
|
17
|
+
end
|
|
18
|
+
s += '<code>'
|
|
19
|
+
@code_lines.each do |l|
|
|
20
|
+
s += escape_text(l) + ' </br>' # ADR-188/SRS-096: code content is inert text
|
|
26
21
|
end
|
|
27
|
-
|
|
22
|
+
s += "</code>\n"
|
|
23
|
+
s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -21,7 +21,7 @@ class ControlledParagraph < Paragraph
|
|
|
21
21
|
s += "<table class=\"controlled\">\n"
|
|
22
22
|
s += "\t<thead> <th>#</th> <th></th> <th title=\"Up-links\">UL</th> <th title=\"Down-links\">DL</th> \
|
|
23
23
|
<th title=\"Test Coverage\">COV</th> <th title=\"Decision Record\">DR</th> </thead>\n"
|
|
24
|
-
@@html_table_render_in_progress = true
|
|
24
|
+
@@html_table_render_in_progress = true
|
|
25
25
|
end
|
|
26
26
|
f_text = format_string(@text)
|
|
27
27
|
s += "\t<tr>\n"
|
|
@@ -85,7 +85,7 @@ class ControlledParagraph < Paragraph
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
if @coverage_links
|
|
88
|
-
if tmp = /^(.+)[.]\d+/.match(@coverage_links[0].id)
|
|
88
|
+
if tmp = /^(.+)[.]\d+/.match(@coverage_links[0].id) # guessing that all the links refer to one document
|
|
89
89
|
cov_link_doc_name = tmp[1].downcase
|
|
90
90
|
end
|
|
91
91
|
if @coverage_links.length == 1
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
require_relative 'controlled_table_row'
|
|
2
2
|
require_relative 'text_line'
|
|
3
3
|
|
|
4
|
-
class ControlledTableColumn < TextLine
|
|
4
|
+
class ControlledTableColumn < TextLine
|
|
5
5
|
attr_accessor :text
|
|
6
6
|
|
|
7
|
-
def initialize(text)
|
|
7
|
+
def initialize(text)
|
|
8
8
|
@text = text.strip
|
|
9
9
|
end
|
|
10
10
|
|
|
@@ -17,7 +17,7 @@ end
|
|
|
17
17
|
class RegualrColumn < ControlledTableColumn
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
class TestStepNumberColumn < ControlledTableColumn
|
|
20
|
+
class TestStepNumberColumn < ControlledTableColumn
|
|
21
21
|
attr_accessor :step_number, :row_id
|
|
22
22
|
|
|
23
23
|
def initialize(text)
|
|
@@ -32,7 +32,7 @@ class TestStepNumberColumn < ControlledTableColumn # rubocop:disable Style/Docum
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
class TestStepResultColumn < ControlledTableColumn
|
|
35
|
+
class TestStepResultColumn < ControlledTableColumn
|
|
36
36
|
def to_html
|
|
37
37
|
f_text = format_string(@text)
|
|
38
38
|
case @text.downcase
|
|
@@ -46,7 +46,7 @@ class TestStepResultColumn < ControlledTableColumn # rubocop:disable Style/Docum
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
class TestStepReferenceColumn < ControlledTableColumn
|
|
49
|
+
class TestStepReferenceColumn < ControlledTableColumn
|
|
50
50
|
attr_accessor :up_link_ids, :up_link_doc_ids, :parent_row
|
|
51
51
|
|
|
52
52
|
def initialize(parent_row, text)
|
|
@@ -99,7 +99,7 @@ class TestStepReferenceColumn < ControlledTableColumn # rubocop:disable Style/Do
|
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
def to_html
|
|
102
|
+
def to_html
|
|
103
103
|
s = ''
|
|
104
104
|
specifications_path = @parent_row.parent_doc.specifications_path
|
|
105
105
|
if @up_link_ids
|
|
@@ -133,10 +133,10 @@ class TestStepReferenceColumn < ControlledTableColumn # rubocop:disable Style/Do
|
|
|
133
133
|
end
|
|
134
134
|
end
|
|
135
135
|
|
|
136
|
-
class ControlledTable < DocItem
|
|
136
|
+
class ControlledTable < DocItem
|
|
137
137
|
attr_accessor :column_names, :rows, :is_separator_detected
|
|
138
138
|
|
|
139
|
-
def initialize(doc, markdown_table)
|
|
139
|
+
def initialize(doc, markdown_table)
|
|
140
140
|
@parent_doc = doc
|
|
141
141
|
@parent_heading = doc.headings[-1]
|
|
142
142
|
|
|
@@ -158,7 +158,7 @@ class ControlledTable < DocItem # rubocop:disable Style/Documentation
|
|
|
158
158
|
true
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
-
def format_columns(columns)
|
|
161
|
+
def format_columns(columns)
|
|
162
162
|
new_row = ControlledTableRow.new
|
|
163
163
|
new_row.parent_doc = @parent_doc
|
|
164
164
|
|
|
@@ -198,11 +198,11 @@ class ControlledTable < DocItem # rubocop:disable Style/Documentation
|
|
|
198
198
|
new_row
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
-
def to_html
|
|
201
|
+
def to_html
|
|
202
202
|
s = ''
|
|
203
203
|
if @@html_table_render_in_progress
|
|
204
204
|
s += "</table>\n"
|
|
205
|
-
@@html_table_render_in_progress = false
|
|
205
|
+
@@html_table_render_in_progress = false
|
|
206
206
|
end
|
|
207
207
|
|
|
208
208
|
s += "<table class=\"markdown_table\">\n"
|
|
@@ -1,23 +1,20 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative 'doc_item'
|
|
2
2
|
|
|
3
3
|
class ControlledTableRow < DocItem
|
|
4
|
+
attr_accessor :id, :up_link_ids, :columns
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@id = ""
|
|
11
|
-
@up_link_ids = nil
|
|
12
|
-
@columns = []
|
|
13
|
-
end
|
|
6
|
+
def initialize
|
|
7
|
+
@id = ''
|
|
8
|
+
@up_link_ids = nil
|
|
9
|
+
@columns = []
|
|
10
|
+
end
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
end
|
|
21
|
-
s += "\t</tr>\n"
|
|
12
|
+
def to_html
|
|
13
|
+
s = ''
|
|
14
|
+
s += "\t<tr>\n"
|
|
15
|
+
@columns.each do |col|
|
|
16
|
+
s += col.to_html # "\t\t<td>#{col}</td>\n\r"
|
|
22
17
|
end
|
|
23
|
-
|
|
18
|
+
s += "\t</tr>\n"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative 'doc_item'
|
|
2
2
|
|
|
3
3
|
class DocFooter < DocItem
|
|
4
|
+
def initialize; end
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
def to_html
|
|
7
|
+
s = ''
|
|
8
|
+
if @@html_table_render_in_progress
|
|
9
|
+
s += "</table>\n"
|
|
10
|
+
@@html_table_render_in_progress = false
|
|
6
11
|
end
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if @@html_table_render_in_progress
|
|
11
|
-
s += "</table>\n"
|
|
12
|
-
@@html_table_render_in_progress = false
|
|
13
|
-
end
|
|
14
|
-
return s
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
end
|
|
12
|
+
s
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'text_line'
|
|
4
4
|
|
|
5
|
-
class DocItem < TextLine
|
|
5
|
+
class DocItem < TextLine
|
|
6
6
|
attr_accessor :parent_doc, :parent_heading
|
|
7
7
|
|
|
8
8
|
@parent_doc = nil
|
|
9
9
|
@parent_heading = nil
|
|
10
10
|
|
|
11
|
-
@@html_table_render_in_progress = false
|
|
11
|
+
@@html_table_render_in_progress = false
|
|
12
12
|
|
|
13
13
|
def initialize(doc)
|
|
14
14
|
super()
|
|
@@ -92,7 +92,7 @@ class Heading < Paragraph
|
|
|
92
92
|
heading_level = level.to_s
|
|
93
93
|
heading_text = get_section_info_html
|
|
94
94
|
if level.zero?
|
|
95
|
-
heading_level = 1.to_s
|
|
95
|
+
heading_level = 1.to_s # Render Doc Title as a regular h1
|
|
96
96
|
heading_text = escape_text(@text) # Doc Title does not have a section number
|
|
97
97
|
end
|
|
98
98
|
s += "<a name=\"#{@anchor_id}\"></a>\n"
|
|
@@ -1,35 +1,33 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative 'doc_item'
|
|
2
2
|
|
|
3
3
|
class Image < DocItem
|
|
4
|
+
attr_accessor :text, :path
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
def initialize(text, path)
|
|
7
|
+
@text = text
|
|
8
|
+
@path = path
|
|
9
|
+
end
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
11
|
+
def getTextWithoutSpaces
|
|
12
|
+
@text.split.join('-')
|
|
13
|
+
end
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
def to_html
|
|
16
|
+
s = ''
|
|
17
|
+
if @@html_table_render_in_progress
|
|
18
|
+
s += "</table>\n"
|
|
19
|
+
@@html_table_render_in_progress = false
|
|
15
20
|
end
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
alt = escape_attr(@text)
|
|
25
|
-
src = safe_url(@path)
|
|
26
|
-
if src.nil? # disallowed scheme: render inert rather than emitting it (ADR-188, SRS-098)
|
|
27
|
-
s += "<p style=\"margin-top: 15px;\">[image: #{alt}]"
|
|
28
|
-
return s
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
s += "<p style=\"margin-top: 15px;\"><img src=\"#{escape_attr(src)}\" alt=\"#{alt}\" "
|
|
32
|
-
s += "href=\"javascript:void(0)\" onclick=\"image_OnClick(this)\">"
|
|
33
|
-
return s
|
|
22
|
+
alt = escape_attr(@text)
|
|
23
|
+
src = safe_url(@path)
|
|
24
|
+
if src.nil? # disallowed scheme: render inert rather than emitting it (ADR-188, SRS-098)
|
|
25
|
+
s += "<p style=\"margin-top: 15px;\">[image: #{alt}]"
|
|
26
|
+
return s
|
|
34
27
|
end
|
|
35
|
-
|
|
28
|
+
|
|
29
|
+
s += "<p style=\"margin-top: 15px;\"><img src=\"#{escape_attr(src)}\" alt=\"#{alt}\" "
|
|
30
|
+
s += 'href="javascript:void(0)" onclick="image_OnClick(this)">'
|
|
31
|
+
s
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -73,7 +73,7 @@ class MarkdownList < DocItem
|
|
|
73
73
|
s.each_char do |c|
|
|
74
74
|
case state
|
|
75
75
|
when 'looking_for_list_item_marker'
|
|
76
|
-
if
|
|
76
|
+
if ['*', '-'].include?(c)
|
|
77
77
|
state = 'looking_for_space'
|
|
78
78
|
elsif numeric?(c)
|
|
79
79
|
state = 'looking_for_dot'
|
|
@@ -106,7 +106,7 @@ class MarkdownList < DocItem
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def self.unordered_list_item?(raw_text)
|
|
109
|
-
res = /^\s*([
|
|
109
|
+
res = /^\s*([*-]\s+)(.*)/.match(raw_text)
|
|
110
110
|
return true if res
|
|
111
111
|
|
|
112
112
|
false
|
|
@@ -6,7 +6,7 @@ class MarkdownTable < DocItem
|
|
|
6
6
|
attr_accessor :column_names, :rows, :heading_row, :is_separator_detected, :column_aligns,
|
|
7
7
|
:is_decision_status_table
|
|
8
8
|
|
|
9
|
-
def initialize(doc, heading_row)
|
|
9
|
+
def initialize(doc, heading_row)
|
|
10
10
|
super(doc)
|
|
11
11
|
@heading_row = heading_row
|
|
12
12
|
|
|
@@ -54,6 +54,13 @@ class MarkdownTable < DocItem
|
|
|
54
54
|
true
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Header-addressed cell grid (each row an array of stripped cell strings).
|
|
58
|
+
# Named alongside ScopeTable#cells so Decision's Scope/Status/version readers
|
|
59
|
+
# can treat either table type uniformly (ADR-194).
|
|
60
|
+
def cells
|
|
61
|
+
@rows
|
|
62
|
+
end
|
|
63
|
+
|
|
57
64
|
def to_html
|
|
58
65
|
s = ''
|
|
59
66
|
if @@html_table_render_in_progress
|
|
@@ -80,7 +87,7 @@ class MarkdownTable < DocItem
|
|
|
80
87
|
row.each_with_index do |col, index|
|
|
81
88
|
cell = col
|
|
82
89
|
cell = '▶' if @is_decision_status_table && index.zero? && col.strip == '*'
|
|
83
|
-
if cell.to_i.positive? && cell.to_i.to_s == cell
|
|
90
|
+
if cell.to_i.positive? && cell.to_i.to_s == cell # autoalign cells with numbers
|
|
84
91
|
s += "\t\t<td style=\"text-align: center;\">#{cell}</td>\n"
|
|
85
92
|
else
|
|
86
93
|
align = ''
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require_relative 'controlled_table'
|
|
5
|
+
require_relative 'work_item'
|
|
6
|
+
|
|
7
|
+
# Purpose-built controlled table for a Decision Record's `# Scope` section
|
|
8
|
+
# (ADR-194). It is modelled on ControlledTable but addresses its columns by
|
|
9
|
+
# header text (not position) and has no fixed link column: it locates `#`,
|
|
10
|
+
# `Item`, `Owner`, `Depends On`, and `Status` wherever the author placed them.
|
|
11
|
+
#
|
|
12
|
+
# Each data row becomes a WorkItem (the dependency-network node). The table also
|
|
13
|
+
# keeps a plain header-addressed cell grid (`cells`) so the ADR-193 Scope
|
|
14
|
+
# readers (owners / WIP / dates) keep working unchanged after the Scope section
|
|
15
|
+
# stopped being a plain MarkdownTable.
|
|
16
|
+
class ScopeTable < ControlledTable
|
|
17
|
+
attr_reader :cells, :work_items
|
|
18
|
+
|
|
19
|
+
def initialize(doc, markdown_table)
|
|
20
|
+
@parent_doc = doc
|
|
21
|
+
@parent_heading = doc.headings[-1]
|
|
22
|
+
@column_names = markdown_table.column_names
|
|
23
|
+
@is_separator_detected = markdown_table.is_separator_detected
|
|
24
|
+
@cells = []
|
|
25
|
+
@work_items = []
|
|
26
|
+
@col = locate_columns(@column_names)
|
|
27
|
+
# The switch to ScopeTable happens on the first data row, so the source
|
|
28
|
+
# MarkdownTable carries none yet; any it did are re-read for safety.
|
|
29
|
+
markdown_table.rows.each { |cells| append_cells(cells) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_row(row)
|
|
33
|
+
append_cells(split_table_cells(row).map(&:strip))
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# True when the table has a leading `#` step column, hence a per-row anchor a
|
|
38
|
+
# dependent record can deep-link to (ADR-194).
|
|
39
|
+
def step_column?
|
|
40
|
+
!@col[:step].nil?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Render as a controlled table: an anchored, centered step number in the `#`
|
|
44
|
+
# column (so rows are linkable, like the protocol table) and formatted text
|
|
45
|
+
# for every other cell.
|
|
46
|
+
def to_html
|
|
47
|
+
s = +''
|
|
48
|
+
if @@html_table_render_in_progress
|
|
49
|
+
s << "</table>\n"
|
|
50
|
+
@@html_table_render_in_progress = false
|
|
51
|
+
end
|
|
52
|
+
s << "<table class=\"markdown_table\">\n\t<thead>"
|
|
53
|
+
@column_names.each { |h| s << " <th>#{format_string(h.strip)}</th>" }
|
|
54
|
+
s << " </thead>\n"
|
|
55
|
+
@cells.each_with_index { |cells, i| s << row_html(cells, @work_items[i]) }
|
|
56
|
+
s << "</table>\n"
|
|
57
|
+
s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def locate_columns(column_names)
|
|
63
|
+
index_of = lambda do |name|
|
|
64
|
+
column_names.each_with_index { |h, i| return i if h.to_s.strip == name }
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
{ step: index_of.call('#'), item: index_of.call('Item'), owner: index_of.call('Owner'),
|
|
68
|
+
depends_on: index_of.call('Depends On'), status: index_of.call('Status'),
|
|
69
|
+
est_focused: index_of.call('Est (focused)'), est_safe: index_of.call('Est (safe)'),
|
|
70
|
+
start_date: index_of.call('Start Date'), target_date: index_of.call('Target Date') }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def append_cells(cells)
|
|
74
|
+
@cells << cells
|
|
75
|
+
@work_items << build_work_item(cells)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_work_item(cells)
|
|
79
|
+
WorkItem.new(
|
|
80
|
+
record_id: @parent_doc.id,
|
|
81
|
+
step: step_number(cells, @cells.length), # 1-based row order fallback
|
|
82
|
+
activity: cell(cells, @col[:item]),
|
|
83
|
+
owner: cell(cells, @col[:owner]),
|
|
84
|
+
status: cell(cells, @col[:status]),
|
|
85
|
+
depends_on_refs: parse_depends_on(cell(cells, @col[:depends_on])),
|
|
86
|
+
start_date: parse_date(cell(cells, @col[:start_date])),
|
|
87
|
+
target_date: parse_date(cell(cells, @col[:target_date])),
|
|
88
|
+
**estimate_attrs(cells)
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A Scope Start/Target Date cell as a Date (ADR-213), or nil when blank or not
|
|
93
|
+
# in the DD-MM-YYYY form the rest of the planning views read (ADR-205).
|
|
94
|
+
def parse_date(text)
|
|
95
|
+
match = /\A(\d{2})-(\d{2})-(\d{4})\z/.match(text.to_s.strip)
|
|
96
|
+
return nil unless match
|
|
97
|
+
|
|
98
|
+
Date.new(match[3].to_i, match[2].to_i, match[1].to_i)
|
|
99
|
+
rescue ArgumentError
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The ADR-195 estimate-derived attributes: focused and safe working-day
|
|
104
|
+
# estimates (blank/unparseable -> 0) and the owning record's sequence number
|
|
105
|
+
# (the critical-chain priority tiebreak).
|
|
106
|
+
def estimate_attrs(cells)
|
|
107
|
+
{
|
|
108
|
+
focused_estimate: parse_estimate(cell(cells, @col[:est_focused])),
|
|
109
|
+
safe_estimate: parse_estimate(cell(cells, @col[:est_safe])),
|
|
110
|
+
record_sequence: @parent_doc.sequence_number.to_i
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A Scope estimate cell as a non-negative number of working days (decimals
|
|
115
|
+
# allowed); blank, unparseable, or negative values count as 0 (ADR-195).
|
|
116
|
+
def parse_estimate(text)
|
|
117
|
+
value = Float(text.to_s.strip, exception: false)
|
|
118
|
+
value&.positive? ? value : 0.0
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The `#` column's integer when present; otherwise the row's 1-based position,
|
|
122
|
+
# so a record without the column keeps each row as its own sequential step.
|
|
123
|
+
def step_number(cells, row_position)
|
|
124
|
+
return row_position if @col[:step].nil?
|
|
125
|
+
|
|
126
|
+
value = cell(cells, @col[:step]).to_s.strip
|
|
127
|
+
value.match?(/\A\d+\z/) ? value.to_i : row_position
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def parse_depends_on(text)
|
|
131
|
+
text.to_s.scan(/>\[([^\]]+)\]/).flatten.map(&:strip)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def cell(cells, index)
|
|
135
|
+
return '' if index.nil?
|
|
136
|
+
|
|
137
|
+
cells[index].to_s.strip
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def row_html(cells, work_item)
|
|
141
|
+
s = +"\t<tr>\n"
|
|
142
|
+
cells.each_with_index { |value, index| s << cell_html(value, index, work_item) }
|
|
143
|
+
s << "\t</tr>\n"
|
|
144
|
+
s
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def cell_html(value, index, work_item)
|
|
148
|
+
return depends_on_html(work_item) if index == @col[:depends_on]
|
|
149
|
+
return step_cell_html(value, work_item) if index == @col[:step]
|
|
150
|
+
return date_cell_html(value) if [@col[:start_date], @col[:target_date]].include?(index)
|
|
151
|
+
|
|
152
|
+
"\t\t<td>#{format_string(value.strip)}</td>\n"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The Start/Target Date cells are kept on one line so a DD-MM-YYYY date never
|
|
156
|
+
# wraps at its hyphens in a narrow column (ADR-213).
|
|
157
|
+
def date_cell_html(value)
|
|
158
|
+
"\t\t<td class=\"scope_date\">#{format_string(value.strip)}</td>\n"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The `#` cell is a named anchor (id + name), mirroring the test-step number
|
|
162
|
+
# column, so a dependent record's Depends On link can target this row.
|
|
163
|
+
def step_cell_html(value, work_item)
|
|
164
|
+
anchor = work_item.row_anchor
|
|
165
|
+
"\t\t<td style=\"text-align: center;\">" \
|
|
166
|
+
"<a name=\"#{anchor}\" id=\"#{anchor}\" href=\"##{anchor}\">#{format_string(value.strip)}</a></td>\n"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Render each authored Depends On reference as a link to the resolved target
|
|
170
|
+
# record. When the target record has a `#` step column, the link deep-jumps to
|
|
171
|
+
# the aligned activity row (its step anchor); otherwise it opens the record
|
|
172
|
+
# page (no such anchor exists). Unresolved references render as broken-link
|
|
173
|
+
# spans (ADR-194 also reports them on the console).
|
|
174
|
+
def depends_on_html(work_item)
|
|
175
|
+
return "\t\t<td></td>\n" if work_item.depends_on_refs.empty?
|
|
176
|
+
|
|
177
|
+
links = work_item.depends_on_refs.map { |ref| dependency_ref_html(ref, work_item) }
|
|
178
|
+
"\t\t<td>#{links.join(', ')}</td>\n"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def dependency_ref_html(ref, work_item)
|
|
182
|
+
dep = work_item.resolved_dependencies[ref]
|
|
183
|
+
return "<span class=\"broken_link\" title=\"Unresolved Depends On\">#{ref}</span>" if dep.nil?
|
|
184
|
+
|
|
185
|
+
href = RelativeUrl.between(@parent_doc.output_rel_path, dep[:doc].output_rel_path, fragment: dep[:anchor])
|
|
186
|
+
"<a href=\"#{href}\" class=\"external\" title=\"Depends on #{dep[:label]}\">#{ref}</a>"
|
|
187
|
+
end
|
|
188
|
+
end
|