Almirah 0.4.0 → 0.4.2
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/console_reporter.rb +46 -0
- data/lib/almirah/doc_fabric.rb +1 -6
- data/lib/almirah/doc_items/code_block.rb +1 -1
- data/lib/almirah/doc_items/controlled_table.rb +1 -1
- data/lib/almirah/doc_items/doc_item.rb +12 -0
- data/lib/almirah/doc_items/heading.rb +22 -4
- data/lib/almirah/doc_items/image.rb +8 -1
- data/lib/almirah/doc_items/markdown_table.rb +3 -3
- data/lib/almirah/doc_items/text_line.rb +145 -29
- data/lib/almirah/doc_types/base_document.rb +21 -38
- data/lib/almirah/doc_types/decision.rb +7 -1
- data/lib/almirah/doc_types/decisions_overview.rb +47 -2
- data/lib/almirah/doc_types/source_file.rb +4 -8
- data/lib/almirah/doc_types/traceability.rb +2 -2
- data/lib/almirah/dom/doc_section.rb +1 -1
- data/lib/almirah/dom/document.rb +1 -1
- data/lib/almirah/html_safe.rb +39 -0
- data/lib/almirah/link_registry.rb +53 -0
- data/lib/almirah/project/project_data.rb +6 -2
- data/lib/almirah/project.rb +60 -14
- data/lib/almirah/project_configuration.rb +1 -1
- data/lib/almirah/project_template.rb +89 -1
- data/lib/almirah/relative_url.rb +22 -0
- data/lib/almirah/search/specifications_db.rb +4 -4
- data/lib/almirah/templates/css/main.css +13 -1
- data/lib/almirah/templates/scripts/main.js +4 -1
- data/lib/almirah/templates/scripts/orama_search.js +20 -7
- metadata +5 -1
|
@@ -25,7 +25,6 @@ class SourceFile < PersistentDocument
|
|
|
25
25
|
depth = relative_path.count('/') + 1 # +1 for the repository folder
|
|
26
26
|
depth += 1 # for the source_files folder
|
|
27
27
|
@specifications_path = "./#{'../' * depth}specifications/"
|
|
28
|
-
puts @specifications_path
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
def to_console
|
|
@@ -78,13 +77,10 @@ class SourceFile < PersistentDocument
|
|
|
78
77
|
@html_file_path = output_file_path
|
|
79
78
|
FileUtils.mkdir_p(File.dirname(output_file_path))
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
css_path = "#{'../' * depth}css/main.css"
|
|
86
|
-
js_path = "#{'../' * depth}scripts/main.js"
|
|
87
|
-
index_path = "#{'../' * depth}index.html"
|
|
80
|
+
@output_rel_path = output_file_path.split('/build/', 2).last
|
|
81
|
+
css_path = rel_to('css/main.css')
|
|
82
|
+
js_path = rel_to('scripts/main.js')
|
|
83
|
+
index_path = rel_to('index.html')
|
|
88
84
|
|
|
89
85
|
file = File.open(output_file_path, 'w')
|
|
90
86
|
file_data.each do |s|
|
|
@@ -75,7 +75,7 @@ class Traceability < BaseDocument
|
|
|
75
75
|
top_item.down_links.each do |bottom_item|
|
|
76
76
|
id_color = "style='background-color: ##{bottom_item.parent_doc.color};'"
|
|
77
77
|
bottom_f_text = bottom_item.format_string( bottom_item.text )
|
|
78
|
-
document_section = bottom_item.parent_heading.
|
|
78
|
+
document_section = bottom_item.parent_heading.get_section_info_html
|
|
79
79
|
s += "\t<tr>\n"
|
|
80
80
|
s += "\t\t<td class=\"item_id\"><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
|
|
81
81
|
s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
|
|
@@ -105,7 +105,7 @@ class Traceability < BaseDocument
|
|
|
105
105
|
if bottom_item.parent_doc.id == @bottom_doc.id
|
|
106
106
|
|
|
107
107
|
bottom_f_text = bottom_item.format_string( bottom_item.text )
|
|
108
|
-
document_section = bottom_item.parent_heading.
|
|
108
|
+
document_section = bottom_item.parent_heading.get_section_info_html
|
|
109
109
|
|
|
110
110
|
s += "\t<tr>\n"
|
|
111
111
|
s += "\t\t<td class=\"item_id\" #{id_color}><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
|
|
@@ -11,7 +11,7 @@ class DocSection
|
|
|
11
11
|
s = ''
|
|
12
12
|
s += "\t<li onclick=\"nav_toggle_expand_list(this, event)\">" \
|
|
13
13
|
'<span class="fa-li"><i class="fa fa-minus-square-o"> </i></span>'
|
|
14
|
-
s += "<a href=\"##{@heading.anchor_id}\">#{@heading.
|
|
14
|
+
s += "<a href=\"##{@heading.anchor_id}\">#{@heading.get_section_info_html}</a>\n"
|
|
15
15
|
unless @sections.empty?
|
|
16
16
|
s += "\t\t<ul class=\"fa-ul\">\n"
|
|
17
17
|
@sections.each do |sub_section|
|
data/lib/almirah/dom/document.rb
CHANGED
|
@@ -64,7 +64,7 @@ class Document
|
|
|
64
64
|
|
|
65
65
|
def section_tree_to_html
|
|
66
66
|
s = ''
|
|
67
|
-
s += "<a href=\"##{@root_section.heading.anchor_id}\">#{@root_section.heading.
|
|
67
|
+
s += "<a href=\"##{@root_section.heading.anchor_id}\">#{@root_section.heading.get_section_info_html}</a>\n"
|
|
68
68
|
unless @root_section.sections.empty?
|
|
69
69
|
s += "\t<ul class=\"fa-ul\" style=\"margin-top: 2px;\">\n"
|
|
70
70
|
@root_section.sections.each do |sub_section|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi'
|
|
4
|
+
|
|
5
|
+
# Shared HTML output-encoding helpers (ADR-188, SRS-096/097/098).
|
|
6
|
+
#
|
|
7
|
+
# Author-written Markdown is untrusted text and must be encoded for its HTML
|
|
8
|
+
# context at the point of output. These helpers are the single mechanism every
|
|
9
|
+
# renderer routes through, so coverage cannot drift item-by-item the way it did
|
|
10
|
+
# when only inline code and wiki-link text were escaped.
|
|
11
|
+
module HtmlSafe
|
|
12
|
+
# URL schemes permitted in link/image targets. Anything else (notably
|
|
13
|
+
# javascript:, data:, vbscript:) is treated as unsafe and rendered inert.
|
|
14
|
+
ALLOWED_URL_SCHEMES = %w[http https mailto].freeze
|
|
15
|
+
|
|
16
|
+
# Escapes literal text rendered into element content (the five characters:
|
|
17
|
+
# & < > " '). Used for paragraph, heading, blockquote, table-cell and fenced
|
|
18
|
+
# code block text. SRS-096.
|
|
19
|
+
def escape_text(str)
|
|
20
|
+
CGI.escapeHTML(str.to_s)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Escapes a value interpolated into a quoted HTML attribute so it cannot
|
|
24
|
+
# terminate the attribute or introduce new attributes/elements. SRS-097.
|
|
25
|
+
def escape_attr(str)
|
|
26
|
+
CGI.escapeHTML(str.to_s)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the URL when it is a relative/anchor reference or carries an allowed
|
|
30
|
+
# scheme; returns nil for any other scheme so the caller can render the
|
|
31
|
+
# link/image inert. SRS-098.
|
|
32
|
+
def safe_url(raw)
|
|
33
|
+
url = raw.to_s.strip
|
|
34
|
+
scheme = url[/\A([a-z][a-z0-9+.-]*):/i, 1]
|
|
35
|
+
return url if scheme.nil? # relative path or anchor reference
|
|
36
|
+
|
|
37
|
+
ALLOWED_URL_SCHEMES.include?(scheme.downcase) ? url : nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Project-wide registry of managed documents, used to resolve cross-document
|
|
4
|
+
# links (ADR-186) to their generated output pages. Documents are indexed by
|
|
5
|
+
# their id / filename stem (case-insensitive, folder-independent) and, when they
|
|
6
|
+
# originate from a source file, by their absolute source path (for native
|
|
7
|
+
# Markdown relative links).
|
|
8
|
+
class LinkRegistry
|
|
9
|
+
attr_reader :collisions
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@by_id = {}
|
|
13
|
+
@by_source = {}
|
|
14
|
+
@collisions = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Each registered document is expected to already carry an output_rel_path
|
|
18
|
+
# (its generated page, relative to the build root).
|
|
19
|
+
def register(doc)
|
|
20
|
+
register_id(doc.id.to_s.downcase, doc)
|
|
21
|
+
return unless doc.respond_to?(:path) && doc.path
|
|
22
|
+
|
|
23
|
+
# Also index by the filename stem so a document resolves by filename, not
|
|
24
|
+
# only by id (SRS-090). For specs/protocols the stem equals the id; for
|
|
25
|
+
# decision records the id is the truncated <letters>-<digits> prefix, so the
|
|
26
|
+
# full stem (e.g. "adr-170-introduce-decision-records") is a distinct alias.
|
|
27
|
+
register_id(File.basename(doc.path, '.*').downcase, doc)
|
|
28
|
+
@by_source[File.expand_path(doc.path)] = doc
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_by_id(id)
|
|
32
|
+
@by_id[id.to_s.downcase]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def find_by_source(source_path)
|
|
36
|
+
@by_source[File.expand_path(source_path)]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Adds an id/stem key for a document, recording a collision only when the key
|
|
42
|
+
# already maps to a different document. Registering the same document under
|
|
43
|
+
# both its id and its filename stem is expected and not a collision.
|
|
44
|
+
def register_id(key, doc)
|
|
45
|
+
return if key.empty?
|
|
46
|
+
|
|
47
|
+
if @by_id.key?(key) && !@by_id[key].equal?(doc)
|
|
48
|
+
@collisions << key
|
|
49
|
+
else
|
|
50
|
+
@by_id[key] = doc
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
require_relative '../link_registry'
|
|
2
|
+
|
|
1
3
|
class ProjectData
|
|
2
4
|
attr_reader :specifications, :protocols, :traceability_matrices, :coverage_matrices, :source_files,
|
|
3
5
|
:specifications_dictionary, :covered_specifications_dictionary, :implemented_specifications_dictionary,
|
|
4
|
-
:implementation_matrices, :decisions
|
|
6
|
+
:implementation_matrices, :decisions, :link_registry
|
|
5
7
|
|
|
6
|
-
def initialize
|
|
8
|
+
def initialize # rubocop:disable Metrics/MethodLength
|
|
7
9
|
@specifications = []
|
|
8
10
|
@protocols = []
|
|
9
11
|
@traceability_matrices = []
|
|
@@ -15,5 +17,7 @@ class ProjectData
|
|
|
15
17
|
@specifications_dictionary = {}
|
|
16
18
|
@covered_specifications_dictionary = {}
|
|
17
19
|
@implemented_specifications_dictionary = {}
|
|
20
|
+
|
|
21
|
+
@link_registry = LinkRegistry.new
|
|
18
22
|
end
|
|
19
23
|
end
|
data/lib/almirah/project.rb
CHANGED
|
@@ -9,6 +9,8 @@ require_relative 'search/specifications_db'
|
|
|
9
9
|
require_relative 'project/doc_linker'
|
|
10
10
|
require_relative 'project_configuration'
|
|
11
11
|
require_relative 'project/project_data'
|
|
12
|
+
require_relative 'console_reporter'
|
|
13
|
+
require_relative 'relative_url'
|
|
12
14
|
|
|
13
15
|
class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
14
16
|
attr_accessor :index, :project, :configuration, :project_data
|
|
@@ -47,6 +49,7 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
47
49
|
link_all_source_files
|
|
48
50
|
link_all_decisions
|
|
49
51
|
check_wrong_specification_referenced
|
|
52
|
+
build_link_registry
|
|
50
53
|
create_index
|
|
51
54
|
render_all_specifications(@project_data.specifications)
|
|
52
55
|
render_all_specifications(@project_data.traceability_matrices)
|
|
@@ -58,6 +61,8 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
58
61
|
render_all_decisions
|
|
59
62
|
render_index
|
|
60
63
|
create_search_data
|
|
64
|
+
report_broken_links
|
|
65
|
+
report_rendered
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
def specifications_and_results(test_run) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
@@ -70,6 +75,7 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
70
75
|
link_all_source_files
|
|
71
76
|
link_all_decisions
|
|
72
77
|
check_wrong_specification_referenced
|
|
78
|
+
build_link_registry
|
|
73
79
|
create_index
|
|
74
80
|
render_all_specifications(@project_data.specifications)
|
|
75
81
|
render_all_specifications(@project_data.traceability_matrices)
|
|
@@ -81,20 +87,60 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
81
87
|
render_all_decisions
|
|
82
88
|
render_index
|
|
83
89
|
create_search_data
|
|
90
|
+
report_broken_links
|
|
91
|
+
report_rendered
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def report_rendered
|
|
95
|
+
root = @configuration.project_root_directory
|
|
96
|
+
base = root == Dir.pwd ? '.' : root
|
|
97
|
+
ConsoleReporter.result('rendering HTML', File.join(base, 'build', 'index.html'))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Reports cross-document links that could not be resolved (ADR-186, SRS-094),
|
|
101
|
+
# naming the linking document. The build still completes.
|
|
102
|
+
def report_broken_links
|
|
103
|
+
broken = TextLine.broken_links
|
|
104
|
+
return if broken.empty?
|
|
105
|
+
|
|
106
|
+
ConsoleReporter.warn('broken links', broken.length)
|
|
107
|
+
broken.each { |b| puts ConsoleReporter.warn_detail(" #{b[:document] || '?'}: #{b[:target]}") }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Assigns each document its generated output path (relative to the build root)
|
|
111
|
+
# and registers it for cross-document link resolution (ADR-186). Runs after all
|
|
112
|
+
# documents are parsed and before any rendering, so link targets are known.
|
|
113
|
+
def build_link_registry # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
114
|
+
reg = @project_data.link_registry
|
|
115
|
+
TextLine.link_registry = reg
|
|
116
|
+
TextLine.reset_broken_links
|
|
117
|
+
@project_data.specifications.each do |d|
|
|
118
|
+
d.output_rel_path = "specifications/#{d.id}/#{d.id}.html"
|
|
119
|
+
reg.register(d)
|
|
120
|
+
end
|
|
121
|
+
@project_data.protocols.each do |d|
|
|
122
|
+
d.output_rel_path = "tests/protocols/#{d.id}/#{d.id}.html"
|
|
123
|
+
reg.register(d)
|
|
124
|
+
end
|
|
125
|
+
@project_data.decisions.each do |d|
|
|
126
|
+
d.output_rel_path = "decisions/#{d.html_rel_path}"
|
|
127
|
+
reg.register(d)
|
|
128
|
+
end
|
|
129
|
+
@project_data.source_files.each do |d|
|
|
130
|
+
rel = d.path.sub("#{d.root_path}/", '')
|
|
131
|
+
d.output_rel_path = "source_files/#{d.repository}/#{rel}.html"
|
|
132
|
+
reg.register(d)
|
|
133
|
+
end
|
|
84
134
|
end
|
|
85
135
|
|
|
86
136
|
def parse_all_specifications
|
|
87
137
|
path = @configuration.project_root_directory
|
|
88
|
-
# do a lasy pass first to get the list of documents id
|
|
89
138
|
Dir.glob("#{path}/specifications/**/*.md").each do |f|
|
|
90
|
-
DocFabric.add_lazy_doc_id(f)
|
|
91
|
-
end
|
|
92
|
-
# parse documents in the second pass
|
|
93
|
-
Dir.glob("#{path}/specifications/**/*.md").each do |f| # rubocop:disable Style/CombinableLoops
|
|
94
139
|
doc = DocFabric.create_specification(f)
|
|
95
140
|
@project_data.specifications.append(doc)
|
|
96
141
|
@project_data.specifications_dictionary[doc.id.to_s.downcase] = doc
|
|
97
142
|
end
|
|
143
|
+
ConsoleReporter.count('parsing specifications', @project_data.specifications.length)
|
|
98
144
|
end
|
|
99
145
|
|
|
100
146
|
def parse_all_protocols
|
|
@@ -103,6 +149,7 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
103
149
|
doc = DocFabric.create_protocol(f)
|
|
104
150
|
@project_data.protocols.append(doc)
|
|
105
151
|
end
|
|
152
|
+
ConsoleReporter.count('parsing test protocols', @project_data.protocols.length)
|
|
106
153
|
end
|
|
107
154
|
|
|
108
155
|
def parse_all_source_files
|
|
@@ -132,6 +179,7 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
132
179
|
@project_data.decisions.append(doc)
|
|
133
180
|
end
|
|
134
181
|
BaseDocument.show_decisions_link = @project_data.decisions.any?
|
|
182
|
+
ConsoleReporter.count('parsing decisions', @project_data.decisions.length)
|
|
135
183
|
end
|
|
136
184
|
|
|
137
185
|
def parse_test_run(test_run)
|
|
@@ -158,6 +206,7 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
158
206
|
@project_data.traceability_matrices.append doc
|
|
159
207
|
end
|
|
160
208
|
end
|
|
209
|
+
ConsoleReporter.count('traceability matrices', @project_data.traceability_matrices.length)
|
|
161
210
|
end
|
|
162
211
|
|
|
163
212
|
def link_all_protocols # rubocop:disable Metrics/MethodLength
|
|
@@ -174,16 +223,20 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
174
223
|
doc = DocFabric.create_coverage_matrix(value)
|
|
175
224
|
@project_data.coverage_matrices.append doc
|
|
176
225
|
end
|
|
226
|
+
ConsoleReporter.count('coverage matrices', @project_data.coverage_matrices.length)
|
|
177
227
|
end
|
|
178
228
|
|
|
179
229
|
def link_all_decisions
|
|
230
|
+
number_of_links = 0
|
|
180
231
|
@project_data.decisions.each do |d|
|
|
181
232
|
@project_data.specifications.each do |s|
|
|
182
233
|
next unless d.up_link_docs.key?(s.id.to_s)
|
|
183
234
|
|
|
184
235
|
DocLinker.link_decision_to_spec(d, s)
|
|
236
|
+
number_of_links += 1
|
|
185
237
|
end
|
|
186
238
|
end
|
|
239
|
+
ConsoleReporter.count('decision links', number_of_links)
|
|
187
240
|
end
|
|
188
241
|
|
|
189
242
|
def link_all_source_files
|
|
@@ -194,6 +247,7 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
194
247
|
doc = DocFabric.create_implementation_document(value)
|
|
195
248
|
@project_data.implementation_matrices.append doc
|
|
196
249
|
end
|
|
250
|
+
ConsoleReporter.count('implementation matrices', @project_data.implementation_matrices.length)
|
|
197
251
|
end
|
|
198
252
|
|
|
199
253
|
def check_wrong_specification_referenced # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
@@ -267,14 +321,12 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
267
321
|
@index = Index.new(@project)
|
|
268
322
|
end
|
|
269
323
|
|
|
270
|
-
def render_all_specifications(spec_list)
|
|
324
|
+
def render_all_specifications(spec_list)
|
|
271
325
|
path = @configuration.project_root_directory
|
|
272
326
|
|
|
273
327
|
FileUtils.mkdir_p("#{path}/build/specifications")
|
|
274
328
|
|
|
275
329
|
spec_list.each do |doc|
|
|
276
|
-
doc.to_console
|
|
277
|
-
|
|
278
330
|
img_src_dir = "#{path}/specifications/#{doc.id}/img"
|
|
279
331
|
img_dst_dir = "#{path}/build/specifications/#{doc.id}/img"
|
|
280
332
|
|
|
@@ -310,8 +362,6 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
310
362
|
FileUtils.mkdir_p("#{path}/build/source_files")
|
|
311
363
|
|
|
312
364
|
@project_data.source_files.each do |doc|
|
|
313
|
-
doc.to_console
|
|
314
|
-
|
|
315
365
|
doc.to_html("#{path}/build/source_files/")
|
|
316
366
|
end
|
|
317
367
|
end
|
|
@@ -320,8 +370,6 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
320
370
|
path = @configuration.project_root_directory
|
|
321
371
|
|
|
322
372
|
doc = @index
|
|
323
|
-
doc.to_console
|
|
324
|
-
|
|
325
373
|
doc.to_html("#{path}/build/")
|
|
326
374
|
end
|
|
327
375
|
|
|
@@ -332,7 +380,6 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
332
380
|
FileUtils.mkdir_p("#{path}/build/decisions")
|
|
333
381
|
|
|
334
382
|
doc = DocFabric.create_decisions_overview(@project)
|
|
335
|
-
doc.to_console
|
|
336
383
|
doc.to_html("#{path}/build/decisions/")
|
|
337
384
|
end
|
|
338
385
|
|
|
@@ -347,7 +394,6 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
347
394
|
depth = 1 + (out_dir_rel == '.' ? 0 : out_dir_rel.split('/').size)
|
|
348
395
|
doc.root_prefix = '../' * depth
|
|
349
396
|
doc.specifications_path = "./#{doc.root_prefix}specifications/"
|
|
350
|
-
doc.to_console
|
|
351
397
|
doc.to_html(NavigationPane.new(doc), "#{out_dir}/")
|
|
352
398
|
end
|
|
353
399
|
end
|
|
@@ -14,6 +14,7 @@ class ProjectTemplate # rubocop:disable Style/Documentation
|
|
|
14
14
|
create_architecture
|
|
15
15
|
create_tests
|
|
16
16
|
create_test_runs
|
|
17
|
+
create_decisions
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def create_requirements
|
|
@@ -265,7 +266,7 @@ class ProjectTemplate # rubocop:disable Style/Documentation
|
|
|
265
266
|
end
|
|
266
267
|
|
|
267
268
|
def run_test_003
|
|
268
|
-
path = File.join(@project_root, 'tests/runs/010/tq-
|
|
269
|
+
path = File.join(@project_root, 'tests/runs/010/tq-001')
|
|
269
270
|
FileUtils.mkdir_p path
|
|
270
271
|
|
|
271
272
|
file_content = <<~EOS
|
|
@@ -295,4 +296,91 @@ class ProjectTemplate # rubocop:disable Style/Documentation
|
|
|
295
296
|
file.puts file_content
|
|
296
297
|
file.close
|
|
297
298
|
end
|
|
299
|
+
|
|
300
|
+
def create_decisions
|
|
301
|
+
path = File.join(@project_root, 'decisions')
|
|
302
|
+
FileUtils.mkdir_p path
|
|
303
|
+
|
|
304
|
+
file_content = <<~EOS
|
|
305
|
+
---
|
|
306
|
+
title: "ADR-001: Start Project Decision"
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
# Status
|
|
310
|
+
|
|
311
|
+
| | Date | Status |
|
|
312
|
+
|:---:|---|---|
|
|
313
|
+
| * | #{Time.now.strftime('%d-%m-%Y')} | Proposed |
|
|
314
|
+
|
|
315
|
+
# Context
|
|
316
|
+
|
|
317
|
+
This is an example decision record. It demonstrates how Almirah captures a
|
|
318
|
+
decision and links it to the requirements it affects.
|
|
319
|
+
|
|
320
|
+
# Decision
|
|
321
|
+
|
|
322
|
+
Describe the decision here. The leading "*" in the Status table marks the
|
|
323
|
+
current state of the record.
|
|
324
|
+
|
|
325
|
+
# Scope
|
|
326
|
+
|
|
327
|
+
| Item | Status | Start Date | Target Date | Description |
|
|
328
|
+
|---|---|---|---|---|
|
|
329
|
+
| Requirements | To Do | | | |
|
|
330
|
+
| Code | To Do | | | |
|
|
331
|
+
| Tests | To Do | | | |
|
|
332
|
+
|
|
333
|
+
# Out of Scope
|
|
334
|
+
|
|
335
|
+
The ADR contains the most important sections with no detailed content.
|
|
336
|
+
|
|
337
|
+
# Consequences
|
|
338
|
+
|
|
339
|
+
## Positive
|
|
340
|
+
|
|
341
|
+
An ADR example will clearly show the approach Almirah framework use for any software changes.
|
|
342
|
+
|
|
343
|
+
## Negative
|
|
344
|
+
|
|
345
|
+
The format of this ADR can be different that is required for the real project.
|
|
346
|
+
|
|
347
|
+
## Neutral
|
|
348
|
+
|
|
349
|
+
TBD
|
|
350
|
+
|
|
351
|
+
# Alternatives Considered
|
|
352
|
+
|
|
353
|
+
- **Add an empty `decisions/` directory only.** Rejected: an empty folder neither teaches the format nor causes the overview page to render, so it would not exercise the feature.
|
|
354
|
+
|
|
355
|
+
# Affected Documents
|
|
356
|
+
|
|
357
|
+
Table below shows a requirement whose text this decision creates or updates.
|
|
358
|
+
|
|
359
|
+
| # | Proposed Text | Req-ID |
|
|
360
|
+
|---|---|---|
|
|
361
|
+
| 1 | This is a first requirement (controlled paragraph with ID equal to "REQ-001"). | >[REQ-001] |
|
|
362
|
+
|
|
363
|
+
# Software Versions
|
|
364
|
+
|
|
365
|
+
| Software Version Category | Software Version ID |
|
|
366
|
+
|---|---|
|
|
367
|
+
| Latest Released Version | n/a |
|
|
368
|
+
| Issue Found in Version | n/a |
|
|
369
|
+
| Target Release Version | 0.0.1 |
|
|
370
|
+
|
|
371
|
+
# References
|
|
372
|
+
|
|
373
|
+
TBD
|
|
374
|
+
|
|
375
|
+
# Review Evidences
|
|
376
|
+
|
|
377
|
+
TBD
|
|
378
|
+
|
|
379
|
+
EOS
|
|
380
|
+
|
|
381
|
+
path = File.join(path, 'adr-001-start-project-decision.md')
|
|
382
|
+
file = File.open(path, 'w')
|
|
383
|
+
file.puts file_content
|
|
384
|
+
file.close
|
|
385
|
+
end
|
|
298
386
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
# Computes the relative URL between two generated pages, each given as a path
|
|
6
|
+
# relative to the build root (e.g. "decisions/release 0.4.1/adr-185.html").
|
|
7
|
+
# Produces forward-slash separators with spaces percent-encoded. Shared by all
|
|
8
|
+
# internal cross-document links (ADR-186).
|
|
9
|
+
module RelativeUrl
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def between(from_output_rel, to_output_rel, fragment: nil)
|
|
13
|
+
from_dir = Pathname.new(from_output_rel).dirname
|
|
14
|
+
rel = Pathname.new(to_output_rel).relative_path_from(from_dir).to_s
|
|
15
|
+
url = rel.split('/').map { |segment| encode_segment(segment) }.join('/')
|
|
16
|
+
fragment && !fragment.empty? ? "#{url}##{fragment}" : url
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def encode_segment(segment)
|
|
20
|
+
segment.gsub(' ', '%20')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -16,7 +16,7 @@ class SpecificationsDb
|
|
|
16
16
|
@specifications.each do |sp|
|
|
17
17
|
sp.items.each do |i|
|
|
18
18
|
if (i.instance_of? Paragraph) or (i.instance_of? ControlledParagraph)
|
|
19
|
-
e = { '
|
|
19
|
+
e = { 'doc_title' => i.parent_doc.title, \
|
|
20
20
|
'doc_color' => i.parent_doc.color, \
|
|
21
21
|
'text' => i.text, \
|
|
22
22
|
'heading_url' => i.parent_heading.get_url, \
|
|
@@ -36,7 +36,7 @@ class SpecificationsDb
|
|
|
36
36
|
item_to_process.rows.each do |r|
|
|
37
37
|
if r.is_a?(MarkdownList)
|
|
38
38
|
f_text = r.text
|
|
39
|
-
e = { '
|
|
39
|
+
e = { 'doc_title' => item_for_reference.parent_doc.title, \
|
|
40
40
|
'doc_color' => item_for_reference.parent_doc.color, \
|
|
41
41
|
'text' => f_text, \
|
|
42
42
|
'heading_url' => item_for_reference.parent_heading.get_url, \
|
|
@@ -45,7 +45,7 @@ class SpecificationsDb
|
|
|
45
45
|
add_markdown_list_item_to_db(data, item_for_reference, r)
|
|
46
46
|
else
|
|
47
47
|
f_text = r
|
|
48
|
-
e = { '
|
|
48
|
+
e = { 'doc_title' => item_for_reference.parent_doc.title, \
|
|
49
49
|
'doc_color' => item_for_reference.parent_doc.color, \
|
|
50
50
|
'text' => f_text, \
|
|
51
51
|
'heading_url' => item_for_reference.parent_heading.get_url, \
|
|
@@ -61,7 +61,7 @@ class SpecificationsDb
|
|
|
61
61
|
item_to_process.rows.each do |row|
|
|
62
62
|
table_text += "| #{row.join(' | ')} |"
|
|
63
63
|
end
|
|
64
|
-
e = { '
|
|
64
|
+
e = { 'doc_title' => item_for_reference.parent_doc.title, \
|
|
65
65
|
'doc_color' => item_for_reference.parent_doc.color, \
|
|
66
66
|
'text' => table_text, \
|
|
67
67
|
'heading_url' => item_for_reference.parent_heading.get_url, \
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
html {
|
|
2
|
+
/* Leave room for the fixed #top_nav so in-page anchor links
|
|
3
|
+
are not hidden behind it when navigating to them. */
|
|
4
|
+
scroll-padding-top: 40px;
|
|
5
|
+
}
|
|
1
6
|
body {
|
|
2
7
|
font-family: Verdana, sans-serif;
|
|
3
8
|
font-size: 12px;
|
|
@@ -154,6 +159,11 @@ a:active {
|
|
|
154
159
|
text-decoration: none;
|
|
155
160
|
display: inline-block;
|
|
156
161
|
}
|
|
162
|
+
a.broken_link, a.broken_link:link, a.broken_link:visited, span.broken_link {
|
|
163
|
+
color: #c00;
|
|
164
|
+
text-decoration: underline wavy #c00;
|
|
165
|
+
cursor: help;
|
|
166
|
+
}
|
|
157
167
|
div.blockquote {
|
|
158
168
|
display: block;
|
|
159
169
|
background:#f9f9fb;
|
|
@@ -219,7 +229,9 @@ img{
|
|
|
219
229
|
background: #EEEEEE;
|
|
220
230
|
border: 1px solid #ddd;
|
|
221
231
|
position: fixed;
|
|
222
|
-
|
|
232
|
+
top: 0; /* Anchor top and bottom so the pane spans exactly the */
|
|
233
|
+
bottom: 0; /* viewport; padding/border stay inside the border box so */
|
|
234
|
+
/* overflow-y scroll can reach the last items. */
|
|
223
235
|
visibility: hidden;
|
|
224
236
|
z-index: 1;
|
|
225
237
|
overflow-y: auto;
|
|
@@ -74,7 +74,10 @@ function openNav() {
|
|
|
74
74
|
|
|
75
75
|
modal.style.display = "block";
|
|
76
76
|
modalImg.src = clicked.src;
|
|
77
|
-
|
|
77
|
+
// Author-supplied alt text: insert as plain text, never as HTML (ADR-188).
|
|
78
|
+
// Reading clicked.alt yields the decoded value, so innerHTML here would
|
|
79
|
+
// re-parse author markup as live DOM (DOM-based XSS).
|
|
80
|
+
captionText.textContent = clicked.alt;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
function modal_close_OnClick(clicked){
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
// Do search only on the Index Page
|
|
2
|
-
import { create, search, insert } from 'https://unpkg.com/@orama/orama@
|
|
2
|
+
import { create, search, insert } from 'https://unpkg.com/@orama/orama@3.1.18/dist/browser/index.js'
|
|
3
|
+
|
|
4
|
+
// Author-derived values are treated as untrusted (ADR-188). Every result field
|
|
5
|
+
// is inserted with safe DOM interfaces (createTextNode / property assignment) and
|
|
6
|
+
// never parsed as HTML, and any URL is admitted only if it is a relative reference
|
|
7
|
+
// or uses an allowed scheme; otherwise the link is rendered inert.
|
|
8
|
+
const ALLOWED_URL_SCHEMES = ['http', 'https', 'mailto'];
|
|
9
|
+
function safeUrl(url) {
|
|
10
|
+
const s = (url == null ? '' : String(url)).trim();
|
|
11
|
+
const m = s.match(/^([a-z][a-z0-9+.-]*):/i);
|
|
12
|
+
if (!m) return s; // relative path or anchor
|
|
13
|
+
return ALLOWED_URL_SCHEMES.includes(m[1].toLowerCase()) ? s : '#';
|
|
14
|
+
}
|
|
3
15
|
|
|
4
16
|
// Create DB
|
|
5
17
|
const db = await create({
|
|
@@ -19,7 +31,7 @@ const data_rows = await response.json();
|
|
|
19
31
|
let i = 0;
|
|
20
32
|
while (i < data_rows.length) {
|
|
21
33
|
await insert(db, {
|
|
22
|
-
|
|
34
|
+
doc_title: data_rows[i]["doc_title"],
|
|
23
35
|
doc_color: data_rows[i]["doc_color"],
|
|
24
36
|
text: data_rows[i]["text"],
|
|
25
37
|
heading_url: data_rows[i]["heading_url"],
|
|
@@ -66,7 +78,7 @@ async function search_onKeyUp(){
|
|
|
66
78
|
}else{
|
|
67
79
|
|
|
68
80
|
searchResult.hits.forEach ((value, index, array) =>{
|
|
69
|
-
const doc_title = value.document["
|
|
81
|
+
const doc_title = value.document["doc_title"]
|
|
70
82
|
const doc_color = value.document["doc_color"]
|
|
71
83
|
const heading_url = value.document["heading_url"]
|
|
72
84
|
const heading_text = value.document["heading_text"]
|
|
@@ -87,19 +99,20 @@ async function search_onKeyUp(){
|
|
|
87
99
|
let cell = document.createElement("td");
|
|
88
100
|
let i = document.createElement("i");
|
|
89
101
|
i.classList.add("fa","fa-file-text-o");
|
|
90
|
-
|
|
102
|
+
if (/^[0-9a-fA-F]{3,8}$/.test(doc_color)) {
|
|
103
|
+
i.style.backgroundColor = "#" + doc_color;
|
|
104
|
+
}
|
|
91
105
|
cell.appendChild(i);
|
|
92
106
|
let textnode = document.createTextNode("\xa0" + doc_title);
|
|
93
107
|
cell.appendChild(textnode);
|
|
94
108
|
row.appendChild(cell)
|
|
95
109
|
cell = document.createElement("td");
|
|
96
110
|
|
|
97
|
-
const a = document.createElement('a');
|
|
111
|
+
const a = document.createElement('a');
|
|
98
112
|
const link = document.createTextNode(heading_text)
|
|
99
113
|
a.appendChild(link);
|
|
100
114
|
a.title = heading_text;
|
|
101
|
-
a.href = heading_url;
|
|
102
|
-
document.body.appendChild(a);
|
|
115
|
+
a.href = safeUrl(heading_url);
|
|
103
116
|
|
|
104
117
|
cell.appendChild(a);
|
|
105
118
|
row.appendChild(cell)
|