Almirah 0.3.1 → 0.4.0
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/lib/almirah/doc_fabric.rb +16 -1
- data/lib/almirah/doc_items/controlled_paragraph.rb +28 -2
- data/lib/almirah/doc_items/controlled_table.rb +4 -3
- data/lib/almirah/doc_items/markdown_list.rb +2 -2
- data/lib/almirah/doc_items/markdown_table.rb +15 -6
- data/lib/almirah/doc_items/text_line.rb +121 -14
- data/lib/almirah/doc_parser.rb +20 -3
- data/lib/almirah/doc_types/base_document.rb +35 -1
- data/lib/almirah/doc_types/decision.rb +161 -0
- data/lib/almirah/doc_types/decisions_overview.rb +158 -0
- data/lib/almirah/doc_types/protocol.rb +3 -0
- data/lib/almirah/project/doc_linker.rb +24 -0
- data/lib/almirah/project/project_data.rb +2 -1
- data/lib/almirah/project.rb +59 -2
- data/lib/almirah/templates/css/main.css +36 -1
- data/lib/almirah/templates/scripts/main.js +7 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3c6b1d68ee25c9b62690c4cdda45ec2d790960a6e75762576ddc60d6c3136e5
|
|
4
|
+
data.tar.gz: 334ef7251595119cd27bf980eeb53a8f02d913cde7d2fc5d0844a1d863b6c825
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20383b85a3d59d35a3c6259095e7e8b6709055b6670e2b651fdecdd814eee54b71ebf7d2cc1958c85bc998a426361dfff470c1c10749044334ae7ec7f1a69616
|
|
7
|
+
data.tar.gz: d1d066ecfc0cfe644eaa28f42d793342e328f42d3eefc0e1fdeb2290c8ff281009bd59b99d23893604f89927ab698a922ef844af1e702c45a3a0a5d51926ff84
|
data/lib/almirah/doc_fabric.rb
CHANGED
|
@@ -5,6 +5,8 @@ require_relative 'doc_types/protocol'
|
|
|
5
5
|
require_relative 'doc_types/coverage'
|
|
6
6
|
require_relative 'doc_types/implementation'
|
|
7
7
|
require_relative 'doc_types/traceability'
|
|
8
|
+
require_relative 'doc_types/decision'
|
|
9
|
+
require_relative 'doc_types/decisions_overview'
|
|
8
10
|
require_relative 'doc_parser'
|
|
9
11
|
require_relative 'source_file_parser'
|
|
10
12
|
require_relative 'dom/document'
|
|
@@ -64,6 +66,19 @@ class DocFabric
|
|
|
64
66
|
doc
|
|
65
67
|
end
|
|
66
68
|
|
|
69
|
+
def self.create_decision(path)
|
|
70
|
+
doc = Decision.new path
|
|
71
|
+
DocFabric.parse_document doc
|
|
72
|
+
doc.extract_current_status
|
|
73
|
+
doc.extract_start_date
|
|
74
|
+
doc.extract_target_release_version
|
|
75
|
+
doc
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.create_decisions_overview(project)
|
|
79
|
+
DecisionsOverview.new project
|
|
80
|
+
end
|
|
81
|
+
|
|
67
82
|
def self.create_source_file(repository_path, path, repository_name)
|
|
68
83
|
doc = SourceFile.new repository_path, path, repository_name
|
|
69
84
|
DocFabric.parse_source_file doc
|
|
@@ -78,7 +93,7 @@ class DocFabric
|
|
|
78
93
|
DocParser.parse(doc, file_lines)
|
|
79
94
|
|
|
80
95
|
# Build dom
|
|
81
|
-
doc.dom = Document.new(doc.headings) if doc.is_a?(Specification) || doc.is_a?(Protocol)
|
|
96
|
+
doc.dom = Document.new(doc.headings) if doc.is_a?(Specification) || doc.is_a?(Protocol) || doc.is_a?(Decision)
|
|
82
97
|
end
|
|
83
98
|
|
|
84
99
|
def self.parse_source_file(doc)
|
|
@@ -2,7 +2,7 @@ require_relative 'paragraph'
|
|
|
2
2
|
|
|
3
3
|
# <REQ> Implementa a controlled paragraph as a subclass of the DocItem >[SRS-001] </REQ>
|
|
4
4
|
class ControlledParagraph < Paragraph
|
|
5
|
-
attr_accessor :id, :up_link_ids, :down_links, :coverage_links, :source_code_links
|
|
5
|
+
attr_accessor :id, :up_link_ids, :down_links, :coverage_links, :source_code_links, :decision_record_links
|
|
6
6
|
|
|
7
7
|
def initialize(doc, text, id)
|
|
8
8
|
super(doc, text)
|
|
@@ -12,6 +12,7 @@ class ControlledParagraph < Paragraph
|
|
|
12
12
|
@down_links = nil
|
|
13
13
|
@coverage_links = nil
|
|
14
14
|
@source_code_links = nil
|
|
15
|
+
@decision_record_links = nil
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def to_html
|
|
@@ -19,7 +20,7 @@ class ControlledParagraph < Paragraph
|
|
|
19
20
|
unless @@html_table_render_in_progress
|
|
20
21
|
s += "<table class=\"controlled\">\n"
|
|
21
22
|
s += "\t<thead> <th>#</th> <th></th> <th title=\"Up-links\">UL</th> <th title=\"Down-links\">DL</th> \
|
|
22
|
-
<th title=\"Test Coverage\">COV</th> </thead>\n"
|
|
23
|
+
<th title=\"Test Coverage\">COV</th> <th title=\"Decision Record\">DR</th> </thead>\n"
|
|
23
24
|
@@html_table_render_in_progress = true # rubocop:disable Style/ClassVars
|
|
24
25
|
end
|
|
25
26
|
f_text = format_string(@text)
|
|
@@ -108,6 +109,31 @@ class ControlledParagraph < Paragraph
|
|
|
108
109
|
else
|
|
109
110
|
s += "\t\t<td class=\"item_id\"></td>\n"
|
|
110
111
|
end
|
|
112
|
+
|
|
113
|
+
if @decision_record_links
|
|
114
|
+
if @decision_record_links.length == 1
|
|
115
|
+
dr_doc = @decision_record_links[0].parent_doc
|
|
116
|
+
s += "\t\t<td class=\"item_id\">\
|
|
117
|
+
<a href=\"./../../decisions/#{dr_doc.html_rel_path}\" \
|
|
118
|
+
class=\"external\" title=\"Decision Record\">#{dr_doc.id.upcase}</a></td>\n"
|
|
119
|
+
else
|
|
120
|
+
s += "\t\t<td class=\"item_id\">"
|
|
121
|
+
s += "<div id=\"DR_#{@id}\" style=\"display: block;\">"
|
|
122
|
+
s += "<a href=\"#\" onclick=\"decisionLink_OnClick(this.parentElement); return false;\" \
|
|
123
|
+
class=\"external\" title=\"Number of decision records\">#{@decision_record_links.length}</a>"
|
|
124
|
+
s += '</div>'
|
|
125
|
+
s += "<div id=\"DRS_#{@id}\" style=\"display: none;\">"
|
|
126
|
+
@decision_record_links.each do |lnk|
|
|
127
|
+
dr_doc = lnk.parent_doc
|
|
128
|
+
s += "\t\t\t<a href=\"./../../decisions/#{dr_doc.html_rel_path}\" \
|
|
129
|
+
class=\"external\" title=\"Referenced in\">#{dr_doc.id.upcase}</a>\n<br>"
|
|
130
|
+
end
|
|
131
|
+
s += '</div>'
|
|
132
|
+
s += "</td>\n"
|
|
133
|
+
end
|
|
134
|
+
else
|
|
135
|
+
s += "\t\t<td class=\"item_id\"></td>\n"
|
|
136
|
+
end
|
|
111
137
|
s += "\t</tr>\n"
|
|
112
138
|
s
|
|
113
139
|
end
|
|
@@ -101,13 +101,14 @@ class TestStepReferenceColumn < ControlledTableColumn # rubocop:disable Style/Do
|
|
|
101
101
|
|
|
102
102
|
def to_html # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
103
103
|
s = ''
|
|
104
|
+
specifications_path = @parent_row.parent_doc.specifications_path
|
|
104
105
|
if @up_link_ids
|
|
105
106
|
if @up_link_ids.length == 1
|
|
106
107
|
if tmp = /^([a-zA-Z]+)-\d+/.match(@up_link_ids[0])
|
|
107
108
|
up_link_doc_name = tmp[1].downcase
|
|
108
109
|
end
|
|
109
110
|
s += "\t\t<td class=\"item_id\" style=\"text-align: center;\">\
|
|
110
|
-
<a href=\"
|
|
111
|
+
<a href=\"#{specifications_path}#{up_link_doc_name}/#{up_link_doc_name}.html##{@up_link_ids[0]}\" \
|
|
111
112
|
class=\"external\" title=\"Linked to\">#{@up_link_ids[0]}</a></td>\n"
|
|
112
113
|
else
|
|
113
114
|
s += "\t\t<td class=\"item_id\" style=\"text-align: center;\">"
|
|
@@ -120,7 +121,7 @@ class TestStepReferenceColumn < ControlledTableColumn # rubocop:disable Style/Do
|
|
|
120
121
|
if tmp = /^([a-zA-Z]+)-\d+/.match(lnk)
|
|
121
122
|
up_link_doc_name = tmp[1].downcase
|
|
122
123
|
end
|
|
123
|
-
s += "\t\t\t<a href=\"
|
|
124
|
+
s += "\t\t\t<a href=\"#{specifications_path}#{up_link_doc_name}/#{up_link_doc_name}.html##{lnk}\" \
|
|
124
125
|
class=\"external\" title=\"Verifies\">#{lnk}</a>\n<br>"
|
|
125
126
|
end
|
|
126
127
|
s += '</div>'
|
|
@@ -184,7 +185,7 @@ class ControlledTable < DocItem # rubocop:disable Style/Documentation
|
|
|
184
185
|
end
|
|
185
186
|
end
|
|
186
187
|
|
|
187
|
-
elsif index + 2 == columns.length
|
|
188
|
+
elsif (index + 2 == columns.length) && @parent_doc.instance_of?(Protocol) # test step result column applies only to Protocols
|
|
188
189
|
|
|
189
190
|
col = TestStepResultColumn.new element
|
|
190
191
|
new_row.columns.append col
|
|
@@ -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 c == '*'
|
|
76
|
+
if c == '*' || 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 =
|
|
109
|
+
res = /^\s*([*\-]\s+)(.*)/.match(raw_text)
|
|
110
110
|
return true if res
|
|
111
111
|
|
|
112
112
|
false
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
require_relative 'doc_item'
|
|
4
4
|
|
|
5
5
|
class MarkdownTable < DocItem
|
|
6
|
-
attr_accessor :column_names, :rows, :heading_row, :is_separator_detected, :column_aligns
|
|
6
|
+
attr_accessor :column_names, :rows, :heading_row, :is_separator_detected, :column_aligns,
|
|
7
|
+
:is_decision_status_table
|
|
7
8
|
|
|
8
|
-
def initialize(doc, heading_row)
|
|
9
|
+
def initialize(doc, heading_row) # rubocop:disable Metrics/MethodLength
|
|
9
10
|
super(doc)
|
|
10
11
|
@heading_row = heading_row
|
|
11
12
|
|
|
@@ -18,6 +19,7 @@ class MarkdownTable < DocItem
|
|
|
18
19
|
@rows = []
|
|
19
20
|
@is_separator_detected = false
|
|
20
21
|
@column_aligns = []
|
|
22
|
+
@is_decision_status_table = false
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def add_separator(line)
|
|
@@ -69,10 +71,17 @@ class MarkdownTable < DocItem
|
|
|
69
71
|
s += " </thead>\n"
|
|
70
72
|
|
|
71
73
|
@rows.each do |row|
|
|
72
|
-
|
|
74
|
+
tr_class = if @is_decision_status_table && row[0].to_s.strip == '*'
|
|
75
|
+
' class="current_status"'
|
|
76
|
+
else
|
|
77
|
+
''
|
|
78
|
+
end
|
|
79
|
+
s += "\t<tr#{tr_class}>\n"
|
|
73
80
|
row.each_with_index do |col, index|
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
cell = col
|
|
82
|
+
cell = '▶' if @is_decision_status_table && index.zero? && col.strip == '*'
|
|
83
|
+
if cell.to_i.positive? && cell.to_i.to_s == cell # autoalign cells with numbers
|
|
84
|
+
s += "\t\t<td style=\"text-align: center;\">#{cell}</td>\n"
|
|
76
85
|
else
|
|
77
86
|
align = ''
|
|
78
87
|
case @column_aligns[index]
|
|
@@ -83,7 +92,7 @@ class MarkdownTable < DocItem
|
|
|
83
92
|
when 'center'
|
|
84
93
|
align = 'style="text-align: center;"'
|
|
85
94
|
end
|
|
86
|
-
f_text = format_string(
|
|
95
|
+
f_text = format_string(cell)
|
|
87
96
|
s += "\t\t<td #{align}>#{f_text}</td>\n"
|
|
88
97
|
end
|
|
89
98
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'cgi'
|
|
2
|
+
|
|
1
3
|
class TextLineToken
|
|
2
4
|
attr_accessor :value
|
|
3
5
|
|
|
@@ -54,14 +56,27 @@ class SquareBracketRightAndParentheseLeft < TextLineToken
|
|
|
54
56
|
end
|
|
55
57
|
end
|
|
56
58
|
|
|
59
|
+
class BacktickToken < TextLineToken
|
|
60
|
+
def initialize # rubocop:disable Lint/MissingSuper
|
|
61
|
+
@value = '`'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class InlineCodeToken < TextLineToken
|
|
66
|
+
def initialize(raw) # rubocop:disable Lint/MissingSuper
|
|
67
|
+
@value = raw
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
57
71
|
class TextLineParser
|
|
58
72
|
attr_accessor :supported_tokens
|
|
59
73
|
|
|
60
|
-
def initialize # rubocop:disable Metrics/AbcSize
|
|
74
|
+
def initialize # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
61
75
|
@supported_tokens = []
|
|
62
76
|
@supported_tokens.append(BoldAndItalicToken.new)
|
|
63
77
|
@supported_tokens.append(BoldToken.new)
|
|
64
78
|
@supported_tokens.append(ItalicToken.new)
|
|
79
|
+
@supported_tokens.append(BacktickToken.new)
|
|
65
80
|
@supported_tokens.append(SquareBracketRightAndParentheseLeft.new)
|
|
66
81
|
@supported_tokens.append(ParentheseLeft.new)
|
|
67
82
|
@supported_tokens.append(ParentheseRight.new)
|
|
@@ -70,7 +85,7 @@ class TextLineParser
|
|
|
70
85
|
@supported_tokens.append(TextLineToken.new)
|
|
71
86
|
end
|
|
72
87
|
|
|
73
|
-
def tokenize(str)
|
|
88
|
+
def tokenize(str) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
|
|
74
89
|
result = []
|
|
75
90
|
sl = str.length
|
|
76
91
|
si = 0
|
|
@@ -79,29 +94,110 @@ class TextLineParser
|
|
|
79
94
|
tl = t.value.length
|
|
80
95
|
if tl != 0 # literal is the last supported token in the list
|
|
81
96
|
projected_end_position = si + tl - 1
|
|
82
|
-
next if projected_end_position
|
|
97
|
+
next if projected_end_position >= sl
|
|
83
98
|
|
|
84
99
|
buf = str[si..projected_end_position]
|
|
85
|
-
|
|
100
|
+
next unless buf == t.value
|
|
101
|
+
|
|
102
|
+
if emphasis_token?(t) && !can_flank?(str, si, projected_end_position)
|
|
103
|
+
append_literal(result, buf)
|
|
104
|
+
else
|
|
86
105
|
result.append(t)
|
|
87
|
-
si = projected_end_position + 1
|
|
88
|
-
break
|
|
89
106
|
end
|
|
107
|
+
si = projected_end_position + 1
|
|
108
|
+
break
|
|
90
109
|
else
|
|
91
|
-
|
|
92
|
-
literal = result[-1]
|
|
93
|
-
literal.value += str[si]
|
|
94
|
-
else
|
|
95
|
-
literal = TextLineToken.new
|
|
96
|
-
literal.value = str[si]
|
|
97
|
-
result.append(literal)
|
|
98
|
-
end
|
|
110
|
+
append_literal(result, str[si])
|
|
99
111
|
si += 1
|
|
100
112
|
end
|
|
101
113
|
end
|
|
102
114
|
end
|
|
115
|
+
fuse_backticks(result)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def fuse_backticks(tokens) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
121
|
+
result = []
|
|
122
|
+
i = 0
|
|
123
|
+
while i < tokens.length
|
|
124
|
+
if tokens[i].instance_of?(BacktickToken)
|
|
125
|
+
closer = next_backtick_index(tokens, i + 1)
|
|
126
|
+
if closer
|
|
127
|
+
raw = tokens[(i + 1)..(closer - 1)].map(&:value).join
|
|
128
|
+
result.append(InlineCodeToken.new(raw))
|
|
129
|
+
i = closer + 1
|
|
130
|
+
else
|
|
131
|
+
append_literal(result, '`')
|
|
132
|
+
i += 1
|
|
133
|
+
end
|
|
134
|
+
else
|
|
135
|
+
result.append(tokens[i])
|
|
136
|
+
i += 1
|
|
137
|
+
end
|
|
138
|
+
end
|
|
103
139
|
result
|
|
104
140
|
end
|
|
141
|
+
|
|
142
|
+
def next_backtick_index(tokens, start_idx)
|
|
143
|
+
idx = start_idx
|
|
144
|
+
while idx < tokens.length
|
|
145
|
+
return idx if tokens[idx].instance_of?(BacktickToken)
|
|
146
|
+
|
|
147
|
+
idx += 1
|
|
148
|
+
end
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def emphasis_token?(token)
|
|
153
|
+
token.is_a?(ItalicToken) || token.is_a?(BoldToken) || token.is_a?(BoldAndItalicToken)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def append_literal(result, text)
|
|
157
|
+
if !result.empty? && result[-1].instance_of?(TextLineToken)
|
|
158
|
+
result[-1].value += text
|
|
159
|
+
else
|
|
160
|
+
literal = TextLineToken.new
|
|
161
|
+
literal.value = text.dup
|
|
162
|
+
result.append(literal)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def can_flank?(str, start_idx, end_idx)
|
|
167
|
+
left_flanking?(str, start_idx, end_idx) || right_flanking?(str, start_idx, end_idx)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def left_flanking?(str, start_idx, end_idx)
|
|
171
|
+
after = char_at(str, end_idx + 1)
|
|
172
|
+
return false if after.nil? || whitespace?(after)
|
|
173
|
+
return true unless punctuation?(after)
|
|
174
|
+
|
|
175
|
+
before = char_at(str, start_idx - 1)
|
|
176
|
+
before.nil? || whitespace?(before)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def right_flanking?(str, start_idx, end_idx)
|
|
180
|
+
before = char_at(str, start_idx - 1)
|
|
181
|
+
return false if before.nil? || whitespace?(before)
|
|
182
|
+
return true unless punctuation?(before)
|
|
183
|
+
|
|
184
|
+
after = char_at(str, end_idx + 1)
|
|
185
|
+
after.nil? || whitespace?(after)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def char_at(str, idx)
|
|
189
|
+
return nil if idx.negative? || idx >= str.length
|
|
190
|
+
|
|
191
|
+
str[idx]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def whitespace?(char)
|
|
195
|
+
char.match?(/\s/)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def punctuation?(char)
|
|
199
|
+
char.match?(/[[:punct:]]/)
|
|
200
|
+
end
|
|
105
201
|
end
|
|
106
202
|
|
|
107
203
|
class TextLineBuilderContext
|
|
@@ -117,6 +213,10 @@ class TextLineBuilderContext
|
|
|
117
213
|
str
|
|
118
214
|
end
|
|
119
215
|
|
|
216
|
+
def inline_code(str)
|
|
217
|
+
str
|
|
218
|
+
end
|
|
219
|
+
|
|
120
220
|
def link(_link_text, link_url)
|
|
121
221
|
link_url
|
|
122
222
|
end
|
|
@@ -230,6 +330,9 @@ class TextLineBuilder
|
|
|
230
330
|
ti = ti_starting_position + 1
|
|
231
331
|
end
|
|
232
332
|
|
|
333
|
+
when 'InlineCodeToken'
|
|
334
|
+
result += @builder_context.inline_code(token_list[ti].value)
|
|
335
|
+
ti += 1
|
|
233
336
|
when 'TextLineToken', 'ParentheseLeft', 'ParentheseRight', 'SquareBracketRight'
|
|
234
337
|
result += token_list[ti].value
|
|
235
338
|
ti += 1
|
|
@@ -267,6 +370,10 @@ class TextLine < TextLineBuilderContext
|
|
|
267
370
|
"<b><i>#{str}</i></b>"
|
|
268
371
|
end
|
|
269
372
|
|
|
373
|
+
def inline_code(str)
|
|
374
|
+
"<code class=\"inline\">#{CGI.escapeHTML(str)}</code>"
|
|
375
|
+
end
|
|
376
|
+
|
|
270
377
|
def link(link_text, link_url)
|
|
271
378
|
# define default result first
|
|
272
379
|
result = "<a target=\"_blank\" rel=\"noopener\" href=\"#{link_url}\" class=\"external\">#{link_text}</a>"
|
data/lib/almirah/doc_parser.rb
CHANGED
|
@@ -174,7 +174,7 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
174
174
|
|
|
175
175
|
doc.items.append(item)
|
|
176
176
|
|
|
177
|
-
elsif res = /^(
|
|
177
|
+
elsif res = /^([*\-]\s+)(.*)/.match(s) # check if unordered list start
|
|
178
178
|
|
|
179
179
|
if doc.title == ''
|
|
180
180
|
# dummy section if root is not a Document Title (level 0)
|
|
@@ -248,8 +248,9 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
248
248
|
|
|
249
249
|
if temp_md_table
|
|
250
250
|
if temp_md_table.is_separator_detected # if there is a separator
|
|
251
|
-
# check if parent doc is a Protocol
|
|
252
|
-
if doc.instance_of?
|
|
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'))
|
|
253
254
|
# check if it is a controlled table
|
|
254
255
|
tmp = /(.*)\s+>\[(\S*)\]/.match(row)
|
|
255
256
|
if tmp && (temp_md_table.instance_of? MarkdownTable)
|
|
@@ -379,4 +380,20 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
379
380
|
end
|
|
380
381
|
temp_md_table
|
|
381
382
|
end
|
|
383
|
+
|
|
384
|
+
# Returns true when the most recently parsed heading is inside (or equal to) a section
|
|
385
|
+
# whose heading text matches `section_name`. Walks back through the heading list
|
|
386
|
+
# accumulating the ancestor chain (strictly decreasing levels).
|
|
387
|
+
def self.in_section?(doc, section_name)
|
|
388
|
+
return false if doc.headings.empty?
|
|
389
|
+
|
|
390
|
+
last_level = doc.headings[-1].level + 1
|
|
391
|
+
doc.headings.reverse_each do |h|
|
|
392
|
+
next if h.level >= last_level
|
|
393
|
+
|
|
394
|
+
last_level = h.level
|
|
395
|
+
return true if h.text.strip == section_name
|
|
396
|
+
end
|
|
397
|
+
false
|
|
398
|
+
end
|
|
382
399
|
end
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
class BaseDocument # rubocop:disable Style/Documentation
|
|
4
4
|
attr_accessor :title, :id, :dom, :headings
|
|
5
5
|
|
|
6
|
+
class << self
|
|
7
|
+
attr_accessor :show_decisions_link
|
|
8
|
+
end
|
|
9
|
+
|
|
6
10
|
def initialize
|
|
7
11
|
@items = []
|
|
8
12
|
@headings = []
|
|
@@ -21,6 +25,10 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
21
25
|
|
|
22
26
|
output_file_path += if @id == 'index'
|
|
23
27
|
"#{@id}.html"
|
|
28
|
+
elsif instance_of? DecisionsOverview
|
|
29
|
+
'overview.html'
|
|
30
|
+
elsif instance_of? Decision
|
|
31
|
+
"#{@id}.html"
|
|
24
32
|
else
|
|
25
33
|
"#{@id}/#{@id}.html"
|
|
26
34
|
end
|
|
@@ -55,14 +63,30 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
55
63
|
elsif instance_of? Protocol
|
|
56
64
|
file.puts '<link rel="stylesheet" href="../../../css/main.css">'
|
|
57
65
|
file.puts '<script src="../../../scripts/main.js"></script>'
|
|
66
|
+
elsif instance_of? DecisionsOverview
|
|
67
|
+
file.puts '<link rel="stylesheet" href="../css/main.css">'
|
|
68
|
+
file.puts '<script src="../scripts/main.js"></script>'
|
|
69
|
+
file.puts '<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>'
|
|
70
|
+
elsif instance_of? Decision
|
|
71
|
+
file.puts "<link rel=\"stylesheet\" href=\"#{root_prefix}css/main.css\">"
|
|
72
|
+
file.puts "<script src=\"#{root_prefix}scripts/main.js\"></script>"
|
|
58
73
|
end
|
|
59
74
|
elsif s.include?('{{HOME_BUTTON}}')
|
|
60
75
|
if @id == 'index'
|
|
61
76
|
file.puts '<a id="home_menu_item" href="./index.html"><span><i class="fa fa-home" aria-hidden="true"></i></span> Home</a>'
|
|
77
|
+
file.puts decisions_link('./decisions/overview.html') if BaseDocument.show_decisions_link
|
|
62
78
|
elsif instance_of? Protocol
|
|
63
79
|
file.puts '<a id="index_menu_item" href="./../../../index.html"><span><i class="fa fa-info" aria-hidden="true"></i></span> Index</a>'
|
|
80
|
+
file.puts decisions_link('./../../../decisions/overview.html') if BaseDocument.show_decisions_link
|
|
81
|
+
elsif instance_of? DecisionsOverview
|
|
82
|
+
file.puts index_link('./../index.html')
|
|
83
|
+
file.puts decisions_link('./overview.html')
|
|
84
|
+
elsif instance_of? Decision
|
|
85
|
+
file.puts index_link("#{root_prefix}index.html")
|
|
86
|
+
file.puts decisions_link("#{root_prefix}decisions/overview.html")
|
|
64
87
|
else
|
|
65
|
-
|
|
88
|
+
file.puts '<a id="index_menu_item" href="./../../index.html"><span><i class="fa fa-info" aria-hidden="true"></i></span> Index</a>'
|
|
89
|
+
file.puts decisions_link('./../../decisions/overview.html') if BaseDocument.show_decisions_link
|
|
66
90
|
end
|
|
67
91
|
elsif s.include?('{{GEM_VERSION}}')
|
|
68
92
|
file.puts "(#{Gem.loaded_specs['Almirah'].version.version})"
|
|
@@ -72,4 +96,14 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
72
96
|
end
|
|
73
97
|
file.close
|
|
74
98
|
end
|
|
99
|
+
|
|
100
|
+
def decisions_link(href)
|
|
101
|
+
icon = '<span><i class="fa fa-gavel" aria-hidden="true"></i></span>'
|
|
102
|
+
%(<a id="decisions_menu_item" href="#{href}">#{icon} Decision Records</a>)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def index_link(href)
|
|
106
|
+
icon = '<span><i class="fa fa-info" aria-hidden="true"></i></span>'
|
|
107
|
+
%(<a id="index_menu_item" href="#{href}">#{icon} Index</a>)
|
|
108
|
+
end
|
|
75
109
|
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require_relative 'persistent_document'
|
|
5
|
+
require_relative '../doc_items/heading'
|
|
6
|
+
require_relative '../doc_items/markdown_table'
|
|
7
|
+
|
|
8
|
+
class Decision < PersistentDocument # rubocop:disable Style/Documentation,Metrics/ClassLength
|
|
9
|
+
attr_accessor :path, :sequence_number, :record_type, :html_rel_path, :root_prefix, :current_status,
|
|
10
|
+
:start_date, :target_release_version, :specifications_path, :wrong_links_hash
|
|
11
|
+
|
|
12
|
+
def initialize(file_path)
|
|
13
|
+
super
|
|
14
|
+
@path = file_path
|
|
15
|
+
stem = File.basename(file_path, File.extname(file_path))
|
|
16
|
+
assign_id_parts(stem)
|
|
17
|
+
@current_status = nil
|
|
18
|
+
@start_date = nil
|
|
19
|
+
@target_release_version = nil
|
|
20
|
+
@wrong_links_hash = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_console
|
|
24
|
+
puts "\e[36mDecision: #{@id}\e[0m"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_html(nav_pane, output_file_path)
|
|
28
|
+
html_rows = []
|
|
29
|
+
html_rows.append('')
|
|
30
|
+
|
|
31
|
+
@items.each do |item|
|
|
32
|
+
html_rows.append item.to_html
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
save_html_to_file(html_rows, nav_pane, output_file_path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def extract_current_status
|
|
39
|
+
status_table = find_section_table('Status')
|
|
40
|
+
return if status_table.nil?
|
|
41
|
+
|
|
42
|
+
status_table.is_decision_status_table = true
|
|
43
|
+
marker_rows = status_table.rows.select { |row| row[0].to_s.strip == '*' }
|
|
44
|
+
@current_status = marker_rows.length == 1 ? marker_rows[0][-1].to_s.strip : nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def extract_start_date
|
|
48
|
+
dates = collect_dates('Status', 'Date') + collect_dates('Scope', 'Start Date')
|
|
49
|
+
@start_date = dates.min
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_target_release_version
|
|
53
|
+
@target_release_version = lookup_cell(
|
|
54
|
+
section_name: 'Software Versions',
|
|
55
|
+
key_column: 'Software Version Category',
|
|
56
|
+
value_column: 'Software Version ID',
|
|
57
|
+
key: 'Target Release Version'
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def effective_status_on(date) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
62
|
+
table = find_section_table('Status')
|
|
63
|
+
return nil if table.nil?
|
|
64
|
+
|
|
65
|
+
date_idx = column_index(table, 'Date')
|
|
66
|
+
status_idx = column_index(table, 'Status')
|
|
67
|
+
return nil if date_idx.nil? || status_idx.nil?
|
|
68
|
+
|
|
69
|
+
best_idx = nil
|
|
70
|
+
best_date = nil
|
|
71
|
+
table.rows.each_with_index do |row, i|
|
|
72
|
+
parsed = parse_dd_mm_yyyy(row[date_idx])
|
|
73
|
+
next if parsed.nil? || parsed > date
|
|
74
|
+
|
|
75
|
+
if best_date.nil? || parsed > best_date || (parsed == best_date && i > best_idx)
|
|
76
|
+
best_date = parsed
|
|
77
|
+
best_idx = i
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
return nil if best_idx.nil?
|
|
81
|
+
|
|
82
|
+
status = table.rows[best_idx][status_idx].to_s.strip
|
|
83
|
+
status.empty? ? nil : status
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def lookup_cell(section_name:, key_column:, value_column:, key:) # rubocop:disable Metrics/AbcSize
|
|
89
|
+
table = find_section_table(section_name)
|
|
90
|
+
return nil if table.nil?
|
|
91
|
+
|
|
92
|
+
key_idx = column_index(table, key_column)
|
|
93
|
+
value_idx = column_index(table, value_column)
|
|
94
|
+
return nil if key_idx.nil? || value_idx.nil?
|
|
95
|
+
|
|
96
|
+
row = table.rows.find { |r| r[key_idx].to_s.strip == key }
|
|
97
|
+
return nil if row.nil?
|
|
98
|
+
|
|
99
|
+
cell = row[value_idx].to_s.strip
|
|
100
|
+
cell.empty? ? nil : cell
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def find_section_table(section_name) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
104
|
+
in_section = false
|
|
105
|
+
section_level = nil
|
|
106
|
+
@items.each do |item|
|
|
107
|
+
if item.is_a?(Heading)
|
|
108
|
+
if !in_section && item.text.strip == section_name
|
|
109
|
+
in_section = true
|
|
110
|
+
section_level = item.level
|
|
111
|
+
elsif in_section && item.level <= section_level
|
|
112
|
+
return nil
|
|
113
|
+
end
|
|
114
|
+
elsif in_section && item.is_a?(MarkdownTable)
|
|
115
|
+
return item
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def collect_dates(section_name, column_name)
|
|
122
|
+
table = find_section_table(section_name)
|
|
123
|
+
return [] if table.nil?
|
|
124
|
+
|
|
125
|
+
col_index = column_index(table, column_name)
|
|
126
|
+
return [] if col_index.nil?
|
|
127
|
+
|
|
128
|
+
table.rows.filter_map { |row| parse_dd_mm_yyyy(row[col_index]) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def column_index(table, column_name)
|
|
132
|
+
table.column_names.each_with_index do |name, idx|
|
|
133
|
+
return idx if name.to_s.strip == column_name
|
|
134
|
+
end
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def parse_dd_mm_yyyy(value)
|
|
139
|
+
return nil if value.nil?
|
|
140
|
+
|
|
141
|
+
match = /\A(\d{2})-(\d{2})-(\d{4})\z/.match(value.to_s.strip)
|
|
142
|
+
return nil unless match
|
|
143
|
+
|
|
144
|
+
Date.new(match[3].to_i, match[2].to_i, match[1].to_i)
|
|
145
|
+
rescue ArgumentError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def assign_id_parts(stem)
|
|
150
|
+
match = stem.match(/\A([A-Za-z]+)-(\d+)/)
|
|
151
|
+
if match
|
|
152
|
+
@id = "#{match[1]}-#{match[2]}".downcase
|
|
153
|
+
@record_type = match[1].upcase
|
|
154
|
+
@sequence_number = match[2]
|
|
155
|
+
else
|
|
156
|
+
@id = stem.downcase
|
|
157
|
+
@record_type = nil
|
|
158
|
+
@sequence_number = nil
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'base_document'
|
|
6
|
+
|
|
7
|
+
class DecisionsOverview < BaseDocument # rubocop:disable Style/Documentation,Metrics/ClassLength
|
|
8
|
+
attr_accessor :project
|
|
9
|
+
|
|
10
|
+
def initialize(project)
|
|
11
|
+
super()
|
|
12
|
+
@project = project
|
|
13
|
+
@title = 'Decision Records Overview'
|
|
14
|
+
@id = 'overview'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_console
|
|
18
|
+
puts "\e[36mDecisions Overview: #{@id}\e[0m"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_html(output_file_path) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
22
|
+
html_rows = []
|
|
23
|
+
html_rows.append('')
|
|
24
|
+
html_rows.append "<h1>#{@title}</h1>\n"
|
|
25
|
+
|
|
26
|
+
html_rows.append render_charts_grid
|
|
27
|
+
|
|
28
|
+
html_rows.append "<table class=\"controlled decisions_overview\">\n"
|
|
29
|
+
html_rows.append "\t<thead>\n"
|
|
30
|
+
html_rows.append "\t\t<th>#</th>\n"
|
|
31
|
+
html_rows.append "\t\t<th>Type</th>\n"
|
|
32
|
+
html_rows.append "\t\t<th>Status</th>\n"
|
|
33
|
+
html_rows.append "\t\t<th>Title</th>\n"
|
|
34
|
+
html_rows.append "\t\t<th>Start Date</th>\n"
|
|
35
|
+
html_rows.append "\t\t<th>Target Date</th>\n"
|
|
36
|
+
html_rows.append "\t\t<th title=\"Target Release Version\">Release</th>\n"
|
|
37
|
+
html_rows.append "\t\t<th>Owner</th>\n"
|
|
38
|
+
html_rows.append "</thead>\n"
|
|
39
|
+
|
|
40
|
+
sorted_items = @project.project_data.decisions.sort_by do |d|
|
|
41
|
+
[d.sequence_number ? 0 : 1, d.sequence_number.to_i, d.id]
|
|
42
|
+
end
|
|
43
|
+
sorted_items.each do |doc|
|
|
44
|
+
s = "\t<tr>\n"
|
|
45
|
+
s += "\t\t<td class=\"item_id\">\n"
|
|
46
|
+
label = doc.sequence_number || doc.id
|
|
47
|
+
href = doc.html_rel_path ? "./#{doc.html_rel_path}" : "##{doc.id}"
|
|
48
|
+
anchor_attrs = %(name="#{doc.id}" id="#{doc.id}" href="#{href}" title="Decision Record ID")
|
|
49
|
+
s += "\t\t\t<a #{anchor_attrs}>#{label}</a>"
|
|
50
|
+
s += "\t\t</td>\n"
|
|
51
|
+
s += "\t\t<td class=\"item_type\">#{doc.record_type}</td>\n"
|
|
52
|
+
s += "\t\t<td class=\"item_status\">#{doc.current_status}</td>\n"
|
|
53
|
+
title_html = doc.html_rel_path ? %(<a href="./#{doc.html_rel_path}" class="external">#{doc.title}</a>) : doc.title
|
|
54
|
+
s += "\t\t<td class=\"item_text\" style='padding: 5px;'>#{title_html}</td>\n"
|
|
55
|
+
start_date_html = doc.start_date ? doc.start_date.strftime('%d-%m-%Y') : ''
|
|
56
|
+
s += "\t\t<td class=\"item_meta\">#{start_date_html}</td>\n"
|
|
57
|
+
s += "\t\t<td class=\"item_meta\"></td>\n"
|
|
58
|
+
s += "\t\t<td class=\"item_meta\">#{doc.target_release_version}</td>\n"
|
|
59
|
+
s += "\t\t<td class=\"item_meta\"></td>\n"
|
|
60
|
+
s += "</tr>\n"
|
|
61
|
+
html_rows.append s
|
|
62
|
+
end
|
|
63
|
+
html_rows.append "</table>\n"
|
|
64
|
+
|
|
65
|
+
save_html_to_file(html_rows, nil, output_file_path)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
CHART_PALETTE = [
|
|
71
|
+
[54, 162, 235], [255, 99, 132], [255, 159, 64], [255, 205, 86],
|
|
72
|
+
[75, 192, 192], [153, 102, 255], [201, 203, 207]
|
|
73
|
+
].freeze
|
|
74
|
+
|
|
75
|
+
def render_charts_grid # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
76
|
+
counts = @project.project_data.decisions.each_with_object(Hash.new(0)) do |item, cntr|
|
|
77
|
+
cntr[item.record_type] += 1 if item.record_type
|
|
78
|
+
end
|
|
79
|
+
labels = counts.keys.sort
|
|
80
|
+
data = labels.map { |k| counts[k] }
|
|
81
|
+
pie_colors = labels.each_with_index.map { |_, i| palette_rgba(i, 0.5) }
|
|
82
|
+
|
|
83
|
+
velocity = velocity_chart_data
|
|
84
|
+
|
|
85
|
+
<<~HTML
|
|
86
|
+
<div class="decisions_overview_charts">
|
|
87
|
+
\t<div class="chart_cell">
|
|
88
|
+
\t\t<canvas id="decisions_type_pie"></canvas>
|
|
89
|
+
\t\t<script>
|
|
90
|
+
\t\t\tnew Chart(document.getElementById('decisions_type_pie'), {
|
|
91
|
+
\t\t\t\ttype: 'pie',
|
|
92
|
+
\t\t\t\tdata: {
|
|
93
|
+
\t\t\t\t\tlabels: #{labels.to_json},
|
|
94
|
+
\t\t\t\t\tdatasets: [{
|
|
95
|
+
\t\t\t\t\t\tlabel: 'Decision records',
|
|
96
|
+
\t\t\t\t\t\tdata: #{data.to_json},
|
|
97
|
+
\t\t\t\t\t\tbackgroundColor: #{pie_colors.to_json},
|
|
98
|
+
\t\t\t\t\t\tborderWidth: 0
|
|
99
|
+
\t\t\t\t\t}]
|
|
100
|
+
\t\t\t\t},
|
|
101
|
+
\t\t\t\toptions: { plugins: { title: { display: true, text: 'Decision Records by Type' } } }
|
|
102
|
+
\t\t\t});
|
|
103
|
+
\t\t</script>
|
|
104
|
+
\t</div>
|
|
105
|
+
\t<div class="chart_cell">
|
|
106
|
+
\t\t<canvas id="decisions_velocity_bar"></canvas>
|
|
107
|
+
\t\t<script>
|
|
108
|
+
\t\t\tnew Chart(document.getElementById('decisions_velocity_bar'), {
|
|
109
|
+
\t\t\t\ttype: 'bar',
|
|
110
|
+
\t\t\t\tdata: #{velocity.to_json},
|
|
111
|
+
\t\t\t\toptions: {
|
|
112
|
+
\t\t\t\t\tplugins: { title: { display: true, text: 'Decision Records by Status Over Time' } },
|
|
113
|
+
\t\t\t\t\tscales: { x: { stacked: true }, y: { stacked: true, ticks: { precision: 0 } } }
|
|
114
|
+
\t\t\t\t}
|
|
115
|
+
\t\t\t});
|
|
116
|
+
\t\t</script>
|
|
117
|
+
\t</div>
|
|
118
|
+
\t<div class="chart_cell"></div>
|
|
119
|
+
</div>
|
|
120
|
+
HTML
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def velocity_chart_data(reference_date: Date.today, weeks: 6) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
124
|
+
fridays = recent_fridays(reference_date, weeks)
|
|
125
|
+
segments = []
|
|
126
|
+
counts = {}
|
|
127
|
+
|
|
128
|
+
fridays.each_with_index do |friday, i|
|
|
129
|
+
@project.project_data.decisions.each do |doc|
|
|
130
|
+
status = doc.effective_status_on(friday)
|
|
131
|
+
next if status.nil?
|
|
132
|
+
|
|
133
|
+
unless counts.key?(status)
|
|
134
|
+
counts[status] = Array.new(fridays.length, 0)
|
|
135
|
+
segments << status
|
|
136
|
+
end
|
|
137
|
+
counts[status][i] += 1
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
labels: fridays.map { |f| f.strftime('%d-%m-%Y') },
|
|
143
|
+
datasets: segments.map { |s| { label: s, data: counts[s] } }
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def palette_rgba(index, alpha)
|
|
148
|
+
r, g, b = CHART_PALETTE[index % CHART_PALETTE.length]
|
|
149
|
+
"rgba(#{r}, #{g}, #{b}, #{alpha})"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def recent_fridays(reference_date, count)
|
|
153
|
+
friday_wday = 5
|
|
154
|
+
days_back = (reference_date.wday - friday_wday) % 7
|
|
155
|
+
most_recent = reference_date - days_back
|
|
156
|
+
(0...count).to_a.reverse.map { |i| most_recent - (7 * i) }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -2,9 +2,12 @@ require_relative "persistent_document"
|
|
|
2
2
|
|
|
3
3
|
class Protocol < PersistentDocument
|
|
4
4
|
|
|
5
|
+
attr_accessor :specifications_path
|
|
6
|
+
|
|
5
7
|
def initialize(fele_path)
|
|
6
8
|
super
|
|
7
9
|
@id = File.basename(fele_path, File.extname(fele_path)).downcase
|
|
10
|
+
@specifications_path = './../../../specifications/'
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def to_html(nav_pane, output_file_path)
|
|
@@ -44,6 +44,30 @@ class DocLinker
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
def self.link_decision_to_spec(decision, specification) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
48
|
+
top_document = specification
|
|
49
|
+
bottom_document = decision
|
|
50
|
+
|
|
51
|
+
bottom_document.controlled_items.each do |item|
|
|
52
|
+
next unless item.up_link_ids
|
|
53
|
+
|
|
54
|
+
item.up_link_ids.each do |up_lnk|
|
|
55
|
+
if top_document.dictionary.key?(up_lnk.to_s)
|
|
56
|
+
|
|
57
|
+
top_item = top_document.dictionary[up_lnk.to_s]
|
|
58
|
+
|
|
59
|
+
top_item.decision_record_links = [] unless top_item.decision_record_links
|
|
60
|
+
top_item.decision_record_links.append(item)
|
|
61
|
+
elsif tmp = /^([a-zA-Z]+)-\d+/.match(up_lnk)
|
|
62
|
+
# check if there is a non existing link with the right doc_id
|
|
63
|
+
if tmp[1].downcase == top_document.id.downcase
|
|
64
|
+
bottom_document.wrong_links_hash[up_lnk] = item
|
|
65
|
+
end # SRS
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
47
71
|
def self.link_source_file_to_spec(source_file, specification) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
48
72
|
top_document = specification
|
|
49
73
|
bottom_document = source_file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
class ProjectData
|
|
2
2
|
attr_reader :specifications, :protocols, :traceability_matrices, :coverage_matrices, :source_files,
|
|
3
3
|
:specifications_dictionary, :covered_specifications_dictionary, :implemented_specifications_dictionary,
|
|
4
|
-
:implementation_matrices
|
|
4
|
+
:implementation_matrices, :decisions
|
|
5
5
|
|
|
6
6
|
def initialize
|
|
7
7
|
@specifications = []
|
|
@@ -10,6 +10,7 @@ class ProjectData
|
|
|
10
10
|
@coverage_matrices = []
|
|
11
11
|
@source_files = []
|
|
12
12
|
@implementation_matrices = []
|
|
13
|
+
@decisions = []
|
|
13
14
|
|
|
14
15
|
@specifications_dictionary = {}
|
|
15
16
|
@covered_specifications_dictionary = {}
|
data/lib/almirah/project.rb
CHANGED
|
@@ -37,13 +37,15 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
37
37
|
FileUtils.copy_entry(src_folder, dst_folder)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def specifications_and_protocols # rubocop:disable Metrics/MethodLength
|
|
40
|
+
def specifications_and_protocols # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
41
41
|
parse_all_specifications
|
|
42
42
|
parse_all_protocols
|
|
43
43
|
parse_all_source_files
|
|
44
|
+
parse_decisions
|
|
44
45
|
link_all_specifications
|
|
45
46
|
link_all_protocols
|
|
46
47
|
link_all_source_files
|
|
48
|
+
link_all_decisions
|
|
47
49
|
check_wrong_specification_referenced
|
|
48
50
|
create_index
|
|
49
51
|
render_all_specifications(@project_data.specifications)
|
|
@@ -52,17 +54,21 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
52
54
|
render_all_protocols
|
|
53
55
|
render_all_source_files
|
|
54
56
|
render_all_specifications(@project_data.implementation_matrices) # intentionally after source file rendering
|
|
57
|
+
render_decisions_overview
|
|
58
|
+
render_all_decisions
|
|
55
59
|
render_index
|
|
56
60
|
create_search_data
|
|
57
61
|
end
|
|
58
62
|
|
|
59
|
-
def specifications_and_results(test_run) # rubocop:disable Metrics/MethodLength
|
|
63
|
+
def specifications_and_results(test_run) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
60
64
|
parse_all_specifications
|
|
61
65
|
parse_test_run test_run
|
|
62
66
|
parse_all_source_files
|
|
67
|
+
parse_decisions
|
|
63
68
|
link_all_specifications
|
|
64
69
|
link_all_protocols
|
|
65
70
|
link_all_source_files
|
|
71
|
+
link_all_decisions
|
|
66
72
|
check_wrong_specification_referenced
|
|
67
73
|
create_index
|
|
68
74
|
render_all_specifications(@project_data.specifications)
|
|
@@ -71,6 +77,8 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
71
77
|
render_all_protocols
|
|
72
78
|
render_all_source_files
|
|
73
79
|
render_all_specifications(@project_data.implementation_matrices) # intentionally after source file rendering
|
|
80
|
+
render_decisions_overview
|
|
81
|
+
render_all_decisions
|
|
74
82
|
render_index
|
|
75
83
|
create_search_data
|
|
76
84
|
end
|
|
@@ -114,6 +122,18 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
114
122
|
end
|
|
115
123
|
end
|
|
116
124
|
|
|
125
|
+
def parse_decisions
|
|
126
|
+
path = @configuration.project_root_directory
|
|
127
|
+
decisions_root = "#{path}/decisions"
|
|
128
|
+
Dir.glob("#{decisions_root}/**/*.md").each do |f|
|
|
129
|
+
doc = DocFabric.create_decision(f)
|
|
130
|
+
rel_dir = File.dirname(f.sub("#{decisions_root}/", ''))
|
|
131
|
+
doc.html_rel_path = rel_dir == '.' ? "#{doc.id}.html" : "#{rel_dir}/#{doc.id}.html"
|
|
132
|
+
@project_data.decisions.append(doc)
|
|
133
|
+
end
|
|
134
|
+
BaseDocument.show_decisions_link = @project_data.decisions.any?
|
|
135
|
+
end
|
|
136
|
+
|
|
117
137
|
def parse_test_run(test_run)
|
|
118
138
|
path = @configuration.project_root_directory
|
|
119
139
|
Dir.glob("#{path}/tests/runs/#{test_run}/**/*.md").each do |f|
|
|
@@ -156,6 +176,16 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
156
176
|
end
|
|
157
177
|
end
|
|
158
178
|
|
|
179
|
+
def link_all_decisions
|
|
180
|
+
@project_data.decisions.each do |d|
|
|
181
|
+
@project_data.specifications.each do |s|
|
|
182
|
+
next unless d.up_link_docs.key?(s.id.to_s)
|
|
183
|
+
|
|
184
|
+
DocLinker.link_decision_to_spec(d, s)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
159
189
|
def link_all_source_files
|
|
160
190
|
return unless DocLinker.link_all_source_files(@project_data)
|
|
161
191
|
|
|
@@ -295,6 +325,33 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
295
325
|
doc.to_html("#{path}/build/")
|
|
296
326
|
end
|
|
297
327
|
|
|
328
|
+
def render_decisions_overview
|
|
329
|
+
return if @project_data.decisions.empty?
|
|
330
|
+
|
|
331
|
+
path = @configuration.project_root_directory
|
|
332
|
+
FileUtils.mkdir_p("#{path}/build/decisions")
|
|
333
|
+
|
|
334
|
+
doc = DocFabric.create_decisions_overview(@project)
|
|
335
|
+
doc.to_console
|
|
336
|
+
doc.to_html("#{path}/build/decisions/")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def render_all_decisions # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
340
|
+
return if @project_data.decisions.empty?
|
|
341
|
+
|
|
342
|
+
build_decisions_root = "#{@configuration.project_root_directory}/build/decisions"
|
|
343
|
+
@project_data.decisions.each do |doc|
|
|
344
|
+
out_dir_rel = File.dirname(doc.html_rel_path)
|
|
345
|
+
out_dir = out_dir_rel == '.' ? build_decisions_root : "#{build_decisions_root}/#{out_dir_rel}"
|
|
346
|
+
FileUtils.mkdir_p(out_dir)
|
|
347
|
+
depth = 1 + (out_dir_rel == '.' ? 0 : out_dir_rel.split('/').size)
|
|
348
|
+
doc.root_prefix = '../' * depth
|
|
349
|
+
doc.specifications_path = "./#{doc.root_prefix}specifications/"
|
|
350
|
+
doc.to_console
|
|
351
|
+
doc.to_html(NavigationPane.new(doc), "#{out_dir}/")
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
298
355
|
def create_search_data
|
|
299
356
|
db = SpecificationsDb.new @project_data.specifications
|
|
300
357
|
data_path = "#{@configuration.project_root_directory}/build/data"
|
|
@@ -64,11 +64,14 @@ table.markdown_table th{
|
|
|
64
64
|
background-color:#EEEEEE;
|
|
65
65
|
}
|
|
66
66
|
table.markdown_table td{
|
|
67
|
-
border: 1px solid #bbb;
|
|
67
|
+
border: 1px solid #bbb;
|
|
68
68
|
padding: 4px;
|
|
69
69
|
display: table-cell;
|
|
70
70
|
vertical-align: inherit;
|
|
71
71
|
}
|
|
72
|
+
table.markdown_table tr.current_status td {
|
|
73
|
+
background-color: #ffffee;
|
|
74
|
+
}
|
|
72
75
|
table.controlled{
|
|
73
76
|
border: 1px solid #e4e4e4;
|
|
74
77
|
border-collapse: collapse;
|
|
@@ -110,9 +113,32 @@ table.controlled td.item_id {
|
|
|
110
113
|
width: 3%;
|
|
111
114
|
text-align: center;
|
|
112
115
|
}
|
|
116
|
+
table.controlled td.item_type {
|
|
117
|
+
width: 5%;
|
|
118
|
+
text-align: center;
|
|
119
|
+
font-weight: normal;
|
|
120
|
+
}
|
|
121
|
+
table.controlled td.item_status {
|
|
122
|
+
width: 5%;
|
|
123
|
+
text-align: center;
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
}
|
|
113
126
|
table.controlled td.item_text{
|
|
114
127
|
text-align: left;
|
|
115
128
|
}
|
|
129
|
+
.decisions_overview_charts {
|
|
130
|
+
display: grid;
|
|
131
|
+
grid-template-columns: repeat(3, 1fr);
|
|
132
|
+
gap: 16px;
|
|
133
|
+
margin-bottom: 16px;
|
|
134
|
+
}
|
|
135
|
+
.decisions_overview_charts .chart_cell {
|
|
136
|
+
min-height: 240px;
|
|
137
|
+
}
|
|
138
|
+
.decisions_overview_charts .chart_cell canvas {
|
|
139
|
+
max-width: 100%;
|
|
140
|
+
max-height: 300px;
|
|
141
|
+
}
|
|
116
142
|
table.controlled:not(.odd-even) tbody tr:nth-child(odd) { background-color:#f6f7f8; }
|
|
117
143
|
table.controlled:not(.odd-even) tbody tr:nth-child(even) { background-color: #fff; }
|
|
118
144
|
table.controlled:not(.odd-even) tbody tr:nth-child(odd):hover,
|
|
@@ -146,6 +172,15 @@ code {
|
|
|
146
172
|
margin-top: 4px;
|
|
147
173
|
margin-bottom: 4px;
|
|
148
174
|
}
|
|
175
|
+
code.inline {
|
|
176
|
+
display: inline;
|
|
177
|
+
background:#f4f4f4;
|
|
178
|
+
border: 1px solid #ddd;
|
|
179
|
+
border-left: 1px solid #ddd;
|
|
180
|
+
border-radius: 3px;
|
|
181
|
+
padding: 0 4px;
|
|
182
|
+
margin: 0;
|
|
183
|
+
}
|
|
149
184
|
div.todoblock {
|
|
150
185
|
display: block;
|
|
151
186
|
background:#fcc;
|
|
@@ -50,7 +50,13 @@ function openNav() {
|
|
|
50
50
|
clicked.style.display = 'none';
|
|
51
51
|
id_parts = clicked.id.split("_");
|
|
52
52
|
required_id = "COVS_" + id_parts[1];
|
|
53
|
-
document.getElementById(required_id).style.display = 'block';
|
|
53
|
+
document.getElementById(required_id).style.display = 'block';
|
|
54
|
+
}
|
|
55
|
+
function decisionLink_OnClick(clicked){
|
|
56
|
+
clicked.style.display = 'none';
|
|
57
|
+
id_parts = clicked.id.split("_");
|
|
58
|
+
required_id = "DRS_" + id_parts[1];
|
|
59
|
+
document.getElementById(required_id).style.display = 'block';
|
|
54
60
|
}
|
|
55
61
|
function upLink_OnClick(clicked){
|
|
56
62
|
clicked.style.display = 'none';
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: Almirah
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleksandr Ivanov
|
|
@@ -78,6 +78,8 @@ files:
|
|
|
78
78
|
- lib/almirah/doc_parser.rb
|
|
79
79
|
- lib/almirah/doc_types/base_document.rb
|
|
80
80
|
- lib/almirah/doc_types/coverage.rb
|
|
81
|
+
- lib/almirah/doc_types/decision.rb
|
|
82
|
+
- lib/almirah/doc_types/decisions_overview.rb
|
|
81
83
|
- lib/almirah/doc_types/implementation.rb
|
|
82
84
|
- lib/almirah/doc_types/index.rb
|
|
83
85
|
- lib/almirah/doc_types/persistent_document.rb
|