Almirah 0.4.0 → 0.4.1
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 +0 -6
- 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/markdown_table.rb +2 -2
- data/lib/almirah/doc_items/text_line.rb +122 -27
- data/lib/almirah/doc_types/base_document.rb +21 -38
- data/lib/almirah/doc_types/decisions_overview.rb +45 -1
- data/lib/almirah/doc_types/source_file.rb +4 -8
- data/lib/almirah/link_registry.rb +38 -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 +5 -0
- data/lib/almirah/templates/scripts/orama_search.js +3 -3
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b4a6b0c92714af081d54b235449ab825fa86842d61869f31cd331a44c702efdc
|
|
4
|
+
data.tar.gz: cf2074e5c3f500689359912872166c1f13c08e59bde29408c90ee7b34fbe18fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 21654dccede23f1828f02b4446f11f626d12ee5d7d3689b96636d6b1be0465de44fddbacd05606935326a27d4317bfaf0b5596813e907a38580ea6591bc430ca
|
|
7
|
+
data.tar.gz: d3821259aeb7e5ff6306c3d9ccec25720030e659e351cd2c32a7791a8520ddea0039e6cab7f083b0244c82095e7c219c14de465bcda55b326eca184d06ce3bc7
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Reporter prints concise, aligned progress lines for the processing pipeline:
|
|
4
|
+
#
|
|
5
|
+
# parsing specifications ..... 4 ok
|
|
6
|
+
# parsing test protocols ..... 2 ok
|
|
7
|
+
# parsing decisions .......... 14 ok
|
|
8
|
+
# traceability matrices ...... 4 ok
|
|
9
|
+
# coverage matrices .......... 1 ok
|
|
10
|
+
# implementation matrices .... 1 ok
|
|
11
|
+
# decision links ............. 5 ok
|
|
12
|
+
# rendering HTML ............. ./build/index.html
|
|
13
|
+
#
|
|
14
|
+
# ANSI colour is applied only when writing to an interactive terminal, so piped
|
|
15
|
+
# or captured output stays clean.
|
|
16
|
+
module ConsoleReporter
|
|
17
|
+
COLUMN = 28 # label is dot-padded out to this column, then the value follows
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def count(label, number)
|
|
22
|
+
emit(label, "#{number} ok", 92) # green highlight
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def result(label, value)
|
|
26
|
+
emit(label, value, 96) # cyan highlight
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def warn(label, value)
|
|
30
|
+
emit(label, value.to_s, 91) # red highlight
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Colourises a detail line with the same red as warn (TTY-gated).
|
|
34
|
+
def warn_detail(text)
|
|
35
|
+
colorize(text, 91)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def emit(label, value, color_code)
|
|
39
|
+
dotted = "#{label} ".ljust(COLUMN, '.')
|
|
40
|
+
puts "#{dotted} #{colorize(value, color_code)}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def colorize(text, color_code)
|
|
44
|
+
$stdout.tty? ? "\e[#{color_code}m#{text}\e[0m" : text
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/almirah/doc_fabric.rb
CHANGED
|
@@ -17,12 +17,6 @@ class DocFabric
|
|
|
17
17
|
@@spec_colors = %w[cff4d2 fbeee6 ffcad4 bce6ff e4dfd9 f9e07f cbe54e d3e7ee eab595 86e3c3
|
|
18
18
|
ffdca2 ffffff ffdd94 d0e6a5 ffb284 f3dbcf c9bbc8 c6c09c]
|
|
19
19
|
|
|
20
|
-
def self.add_lazy_doc_id(path)
|
|
21
|
-
if res = /(\w+)[.]md$/.match(path)
|
|
22
|
-
TextLine.add_lazy_doc_id(res[1])
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
20
|
def self.create_specification(path)
|
|
27
21
|
color = @@spec_colors[@@color_index]
|
|
28
22
|
@@color_index += 1
|
|
@@ -19,4 +19,16 @@ class DocItem < TextLine # rubocop:disable Style/Documentation
|
|
|
19
19
|
def get_url
|
|
20
20
|
''
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
# Splits a Markdown table row into its cell strings, treating a backslash-escaped
|
|
24
|
+
# pipe (\|) as a literal character within a cell rather than a column separator
|
|
25
|
+
# (e.g. a "|" inside an inline code span such as `[[target\|alias]]`). The escaping
|
|
26
|
+
# backslash is dropped so the cell renders the intended literal pipe.
|
|
27
|
+
def split_table_cells(row)
|
|
28
|
+
row.split(/(?<!\\)\|/).map { |cell| cell.gsub('\\|', '|') }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def owner_document
|
|
32
|
+
@parent_doc
|
|
33
|
+
end
|
|
22
34
|
end
|
|
@@ -12,7 +12,7 @@ class MarkdownTable < DocItem
|
|
|
12
12
|
|
|
13
13
|
res = /^[|](.*[|])/.match(heading_row)
|
|
14
14
|
@column_names = if res
|
|
15
|
-
res[1]
|
|
15
|
+
split_table_cells(res[1])
|
|
16
16
|
else
|
|
17
17
|
['# ERROR# ']
|
|
18
18
|
end
|
|
@@ -49,7 +49,7 @@ class MarkdownTable < DocItem
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def add_row(row)
|
|
52
|
-
columns = row
|
|
52
|
+
columns = split_table_cells(row)
|
|
53
53
|
@rows.append(columns.map!(&:strip))
|
|
54
54
|
true
|
|
55
55
|
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require 'cgi'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require_relative '../relative_url'
|
|
2
4
|
|
|
3
5
|
class TextLineToken
|
|
4
6
|
attr_accessor :value
|
|
@@ -50,6 +52,18 @@ class SquareBracketRight < TextLineToken
|
|
|
50
52
|
end
|
|
51
53
|
end
|
|
52
54
|
|
|
55
|
+
class DoubleSquareBracketLeft < TextLineToken
|
|
56
|
+
def initialize # rubocop:disable Lint/MissingSuper
|
|
57
|
+
@value = '[['
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class DoubleSquareBracketRight < TextLineToken
|
|
62
|
+
def initialize # rubocop:disable Lint/MissingSuper
|
|
63
|
+
@value = ']]'
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
53
67
|
class SquareBracketRightAndParentheseLeft < TextLineToken
|
|
54
68
|
def initialize
|
|
55
69
|
@value = ']('
|
|
@@ -77,6 +91,8 @@ class TextLineParser
|
|
|
77
91
|
@supported_tokens.append(BoldToken.new)
|
|
78
92
|
@supported_tokens.append(ItalicToken.new)
|
|
79
93
|
@supported_tokens.append(BacktickToken.new)
|
|
94
|
+
@supported_tokens.append(DoubleSquareBracketLeft.new)
|
|
95
|
+
@supported_tokens.append(DoubleSquareBracketRight.new)
|
|
80
96
|
@supported_tokens.append(SquareBracketRightAndParentheseLeft.new)
|
|
81
97
|
@supported_tokens.append(ParentheseLeft.new)
|
|
82
98
|
@supported_tokens.append(ParentheseRight.new)
|
|
@@ -220,6 +236,10 @@ class TextLineBuilderContext
|
|
|
220
236
|
def link(_link_text, link_url)
|
|
221
237
|
link_url
|
|
222
238
|
end
|
|
239
|
+
|
|
240
|
+
def wiki_link(inner)
|
|
241
|
+
"[[#{inner}]]"
|
|
242
|
+
end
|
|
223
243
|
end
|
|
224
244
|
|
|
225
245
|
class TextLineBuilder
|
|
@@ -229,7 +249,7 @@ class TextLineBuilder
|
|
|
229
249
|
@builder_context = builder_context
|
|
230
250
|
end
|
|
231
251
|
|
|
232
|
-
def restore(token_list)
|
|
252
|
+
def restore(token_list) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
233
253
|
result = ''
|
|
234
254
|
return '' if token_list.nil?
|
|
235
255
|
|
|
@@ -296,6 +316,25 @@ class TextLineBuilder
|
|
|
296
316
|
result += '***'
|
|
297
317
|
ti = ti_starting_position + 1
|
|
298
318
|
end
|
|
319
|
+
when 'DoubleSquareBracketLeft'
|
|
320
|
+
# wiki/Obsidian link: collect raw text up to the closing "]]"
|
|
321
|
+
is_found = false
|
|
322
|
+
ti_starting_position = ti
|
|
323
|
+
tii = ti + 1
|
|
324
|
+
while tii < tl
|
|
325
|
+
if token_list[tii].instance_of?(DoubleSquareBracketRight)
|
|
326
|
+
inner = token_list[(ti + 1)..(tii - 1)].map(&:value).join
|
|
327
|
+
result += @builder_context.wiki_link(inner)
|
|
328
|
+
ti = tii + 1
|
|
329
|
+
is_found = true
|
|
330
|
+
break
|
|
331
|
+
end
|
|
332
|
+
tii += 1
|
|
333
|
+
end
|
|
334
|
+
unless is_found
|
|
335
|
+
result += '[['
|
|
336
|
+
ti = ti_starting_position + 1
|
|
337
|
+
end
|
|
299
338
|
when 'SquareBracketLeft'
|
|
300
339
|
# try to find closing part
|
|
301
340
|
is_found = false
|
|
@@ -333,7 +372,7 @@ class TextLineBuilder
|
|
|
333
372
|
when 'InlineCodeToken'
|
|
334
373
|
result += @builder_context.inline_code(token_list[ti].value)
|
|
335
374
|
ti += 1
|
|
336
|
-
when 'TextLineToken', 'ParentheseLeft', 'ParentheseRight', 'SquareBracketRight'
|
|
375
|
+
when 'TextLineToken', 'ParentheseLeft', 'ParentheseRight', 'SquareBracketRight', 'DoubleSquareBracketRight'
|
|
337
376
|
result += token_list[ti].value
|
|
338
377
|
ti += 1
|
|
339
378
|
else
|
|
@@ -345,11 +384,37 @@ class TextLineBuilder
|
|
|
345
384
|
end
|
|
346
385
|
|
|
347
386
|
class TextLine < TextLineBuilderContext
|
|
348
|
-
@@
|
|
387
|
+
@@link_registry = nil # rubocop:disable Style/ClassVars
|
|
388
|
+
@@broken_links = [] # rubocop:disable Style/ClassVars
|
|
389
|
+
|
|
390
|
+
class << self
|
|
391
|
+
def link_registry=(registry)
|
|
392
|
+
@@link_registry = registry # rubocop:disable Style/ClassVars
|
|
393
|
+
end
|
|
349
394
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
395
|
+
def link_registry
|
|
396
|
+
@@link_registry
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Cross-document links that could not be resolved to a managed document,
|
|
400
|
+
# collected during rendering for reporting (ADR-186, SRS-094).
|
|
401
|
+
def broken_links
|
|
402
|
+
@@broken_links
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def reset_broken_links
|
|
406
|
+
@@broken_links = [] # rubocop:disable Style/ClassVars
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def record_broken_link(document, target)
|
|
410
|
+
@@broken_links << { document: document&.id, target: target }
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# The document that owns this text line. Used to resolve cross-document links
|
|
415
|
+
# relative to the current page. nil for stand-alone text (e.g. unit tests).
|
|
416
|
+
def owner_document
|
|
417
|
+
nil
|
|
353
418
|
end
|
|
354
419
|
|
|
355
420
|
def format_string(str)
|
|
@@ -374,30 +439,60 @@ class TextLine < TextLineBuilderContext
|
|
|
374
439
|
"<code class=\"inline\">#{CGI.escapeHTML(str)}</code>"
|
|
375
440
|
end
|
|
376
441
|
|
|
377
|
-
def link(link_text, link_url)
|
|
378
|
-
|
|
379
|
-
|
|
442
|
+
def link(link_text, link_url) # rubocop:disable Metrics/MethodLength
|
|
443
|
+
raw = link_url.to_s
|
|
444
|
+
kind, target, fragment = classify_markdown_link(raw)
|
|
445
|
+
case kind
|
|
446
|
+
when :internal
|
|
447
|
+
href = RelativeUrl.between(owner_document.output_rel_path, target.output_rel_path, fragment: fragment)
|
|
448
|
+
"<a href=\"#{href}\" class=\"external\">#{link_text}</a>"
|
|
449
|
+
when :broken
|
|
450
|
+
TextLine.record_broken_link(owner_document, raw)
|
|
451
|
+
"<a href=\"#{raw}\" class=\"broken_link\" title=\"Unresolved cross-document link\">#{link_text}</a>"
|
|
452
|
+
else
|
|
453
|
+
"<a target=\"_blank\" rel=\"noopener\" href=\"#{raw}\" class=\"external\">#{link_text}</a>"
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Resolves an Obsidian/wiki link "[[target#fragment|alias]]" to a managed
|
|
458
|
+
# document by its unique id/filename, independent of folder (ADR-186).
|
|
459
|
+
def wiki_link(inner) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize,Metrics/MethodLength
|
|
460
|
+
link_part, sep, alias_text = inner.partition('|')
|
|
461
|
+
target, _hash, fragment = link_part.partition('#')
|
|
462
|
+
display = (sep.empty? ? link_part : alias_text).strip
|
|
463
|
+
display = link_part.strip if display.empty?
|
|
464
|
+
|
|
465
|
+
doc = TextLine.link_registry&.find_by_id(target.strip)
|
|
466
|
+
if doc && owner_document&.output_rel_path
|
|
467
|
+
href = RelativeUrl.between(owner_document.output_rel_path, doc.output_rel_path,
|
|
468
|
+
fragment: fragment.strip.empty? ? nil : fragment.strip)
|
|
469
|
+
"<a href=\"#{href}\" class=\"external\">#{CGI.escapeHTML(display)}</a>"
|
|
470
|
+
elsif owner_document&.output_rel_path
|
|
471
|
+
TextLine.record_broken_link(owner_document, "[[#{inner}]]")
|
|
472
|
+
"<span class=\"broken_link\" title=\"Unresolved wiki link\">#{CGI.escapeHTML(display)}</span>"
|
|
473
|
+
else
|
|
474
|
+
"[[#{inner}]]"
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
private
|
|
380
479
|
|
|
381
|
-
|
|
382
|
-
|
|
480
|
+
# Classifies a Markdown link target as :internal (a managed document, resolved
|
|
481
|
+
# against the owning document's source directory), :broken (a local .md path
|
|
482
|
+
# that does not resolve), or :external (everything else). ADR-186.
|
|
483
|
+
def classify_markdown_link(raw) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
484
|
+
path_part, _sep, fragment = raw.partition('#')
|
|
485
|
+
path_part = URI::DEFAULT_PARSER.unescape(path_part) # decode %20 etc. to match the real file path
|
|
486
|
+
return [:external] unless local_markdown_path?(path_part)
|
|
383
487
|
|
|
384
|
-
|
|
385
|
-
|
|
488
|
+
doc = owner_document
|
|
489
|
+
return [:external] unless doc&.output_rel_path && doc.respond_to?(:path) && doc.path && TextLine.link_registry
|
|
386
490
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
anchor = res[2]
|
|
391
|
-
end
|
|
392
|
-
end
|
|
491
|
+
target = TextLine.link_registry.find_by_source(File.expand_path(path_part, File.dirname(doc.path)))
|
|
492
|
+
target ? [:internal, target, fragment.empty? ? nil : fragment] : [:broken]
|
|
493
|
+
end
|
|
393
494
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
"<a href=\".\\..\\#{lazy_doc_id}\\#{lazy_doc_id}.html#{anchor}\" class=\"external\">#{link_text}</a>"
|
|
397
|
-
else
|
|
398
|
-
"<a href=\".\\..\\#{lazy_doc_id}\\#{lazy_doc_id}.html\" class=\"external\">#{link_text}</a>"
|
|
399
|
-
end
|
|
400
|
-
end
|
|
401
|
-
result
|
|
495
|
+
def local_markdown_path?(path_part)
|
|
496
|
+
path_part.match?(/\.(md|markdown)\z/i) && !path_part.match?(%r{\A[a-z][a-z0-9+.\-]*://}i)
|
|
402
497
|
end
|
|
403
498
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../relative_url'
|
|
4
|
+
|
|
3
5
|
class BaseDocument # rubocop:disable Style/Documentation
|
|
4
|
-
attr_accessor :title, :id, :dom, :headings
|
|
6
|
+
attr_accessor :title, :id, :dom, :headings, :output_rel_path
|
|
5
7
|
|
|
6
8
|
class << self
|
|
7
9
|
attr_accessor :show_decisions_link
|
|
@@ -32,6 +34,7 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
32
34
|
else
|
|
33
35
|
"#{@id}/#{@id}.html"
|
|
34
36
|
end
|
|
37
|
+
@output_rel_path = output_file_path.split('/build/', 2).last
|
|
35
38
|
file = File.open(output_file_path, 'w')
|
|
36
39
|
file_data.each do |s| # rubocop:disable Metrics/BlockLength
|
|
37
40
|
if s.include?('{{CONTENT}}')
|
|
@@ -43,51 +46,21 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
43
46
|
elsif s.include?('{{DOCUMENT_TITLE}}')
|
|
44
47
|
file.puts s.gsub! '{{DOCUMENT_TITLE}}', @title
|
|
45
48
|
elsif s.include?('{{STYLES_AND_SCRIPTS}}')
|
|
49
|
+
file.puts "<link rel=\"stylesheet\" href=\"#{rel_to('css/main.css')}\">"
|
|
50
|
+
file.puts "<script src=\"#{rel_to('scripts/main.js')}\"></script>"
|
|
46
51
|
if @id == 'index'
|
|
47
|
-
file.puts
|
|
48
|
-
file.puts
|
|
49
|
-
file.puts '<link rel="stylesheet" href="./css/main.css">'
|
|
50
|
-
file.puts '<script src="./scripts/main.js"></script>'
|
|
51
|
-
elsif instance_of? Specification
|
|
52
|
-
file.puts '<link rel="stylesheet" href="../../css/main.css">'
|
|
53
|
-
file.puts '<script src="../../scripts/main.js"></script>'
|
|
54
|
-
elsif instance_of? Traceability
|
|
55
|
-
file.puts '<link rel="stylesheet" href="../../css/main.css">'
|
|
56
|
-
file.puts '<script src="../../scripts/main.js"></script>'
|
|
57
|
-
elsif instance_of? Coverage
|
|
58
|
-
file.puts '<link rel="stylesheet" href="../../css/main.css">'
|
|
59
|
-
file.puts '<script src="../../scripts/main.js"></script>'
|
|
60
|
-
elsif instance_of? Implementation
|
|
61
|
-
file.puts '<link rel="stylesheet" href="../../css/main.css">'
|
|
62
|
-
file.puts '<script src="../../scripts/main.js"></script>'
|
|
63
|
-
elsif instance_of? Protocol
|
|
64
|
-
file.puts '<link rel="stylesheet" href="../../../css/main.css">'
|
|
65
|
-
file.puts '<script src="../../../scripts/main.js"></script>'
|
|
52
|
+
file.puts "<script type=\"module\" src=\"#{rel_to('scripts/orama_search.js')}\"></script>"
|
|
53
|
+
file.puts "<link rel=\"stylesheet\" href=\"#{rel_to('css/search.css')}\">"
|
|
66
54
|
elsif instance_of? DecisionsOverview
|
|
67
|
-
file.puts '<link rel="stylesheet" href="../css/main.css">'
|
|
68
|
-
file.puts '<script src="../scripts/main.js"></script>'
|
|
69
55
|
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>"
|
|
73
56
|
end
|
|
74
57
|
elsif s.include?('{{HOME_BUTTON}}')
|
|
75
58
|
if @id == 'index'
|
|
76
|
-
file.puts '
|
|
77
|
-
file.puts decisions_link('./decisions/overview.html') if BaseDocument.show_decisions_link
|
|
78
|
-
elsif instance_of? Protocol
|
|
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")
|
|
59
|
+
file.puts home_link(rel_to('index.html'))
|
|
87
60
|
else
|
|
88
|
-
file.puts '
|
|
89
|
-
file.puts decisions_link('./../../decisions/overview.html') if BaseDocument.show_decisions_link
|
|
61
|
+
file.puts index_link(rel_to('index.html'))
|
|
90
62
|
end
|
|
63
|
+
file.puts decisions_link(rel_to('decisions/overview.html')) if BaseDocument.show_decisions_link
|
|
91
64
|
elsif s.include?('{{GEM_VERSION}}')
|
|
92
65
|
file.puts "(#{Gem.loaded_specs['Almirah'].version.version})"
|
|
93
66
|
else
|
|
@@ -106,4 +79,14 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
106
79
|
icon = '<span><i class="fa fa-info" aria-hidden="true"></i></span>'
|
|
107
80
|
%(<a id="index_menu_item" href="#{href}">#{icon} Index</a>)
|
|
108
81
|
end
|
|
82
|
+
|
|
83
|
+
def home_link(href)
|
|
84
|
+
icon = '<span><i class="fa fa-home" aria-hidden="true"></i></span>'
|
|
85
|
+
%(<a id="home_menu_item" href="#{href}">#{icon} Home</a>)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Relative URL from this page to a target path under the build root.
|
|
89
|
+
def rel_to(target)
|
|
90
|
+
RelativeUrl.between(@output_rel_path, target)
|
|
91
|
+
end
|
|
109
92
|
end
|
|
@@ -72,6 +72,10 @@ class DecisionsOverview < BaseDocument # rubocop:disable Style/Documentation,Met
|
|
|
72
72
|
[75, 192, 192], [153, 102, 255], [201, 203, 207]
|
|
73
73
|
].freeze
|
|
74
74
|
|
|
75
|
+
# Records with a missing or ambiguous current-status marker are surfaced under
|
|
76
|
+
# this category as a data-quality indicator rather than being silently dropped.
|
|
77
|
+
UNDEFINED_STATUS_LABEL = 'Undefined'
|
|
78
|
+
|
|
75
79
|
def render_charts_grid # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
76
80
|
counts = @project.project_data.decisions.each_with_object(Hash.new(0)) do |item, cntr|
|
|
77
81
|
cntr[item.record_type] += 1 if item.record_type
|
|
@@ -81,6 +85,7 @@ class DecisionsOverview < BaseDocument # rubocop:disable Style/Documentation,Met
|
|
|
81
85
|
pie_colors = labels.each_with_index.map { |_, i| palette_rgba(i, 0.5) }
|
|
82
86
|
|
|
83
87
|
velocity = velocity_chart_data
|
|
88
|
+
status_dist = status_distribution_chart_data
|
|
84
89
|
|
|
85
90
|
<<~HTML
|
|
86
91
|
<div class="decisions_overview_charts">
|
|
@@ -115,7 +120,20 @@ class DecisionsOverview < BaseDocument # rubocop:disable Style/Documentation,Met
|
|
|
115
120
|
\t\t\t});
|
|
116
121
|
\t\t</script>
|
|
117
122
|
\t</div>
|
|
118
|
-
\t<div class="chart_cell"
|
|
123
|
+
\t<div class="chart_cell">
|
|
124
|
+
\t\t<canvas id="decisions_status_bar"></canvas>
|
|
125
|
+
\t\t<script>
|
|
126
|
+
\t\t\tnew Chart(document.getElementById('decisions_status_bar'), {
|
|
127
|
+
\t\t\t\ttype: 'bar',
|
|
128
|
+
\t\t\t\tdata: #{status_dist.to_json},
|
|
129
|
+
\t\t\t\toptions: {
|
|
130
|
+
\t\t\t\t\tindexAxis: 'y',
|
|
131
|
+
\t\t\t\t\tplugins: { title: { display: true, text: 'Decision Records by Current Status' }, legend: { display: false } },
|
|
132
|
+
\t\t\t\t\tscales: { x: { beginAtZero: true, ticks: { precision: 0 } } }
|
|
133
|
+
\t\t\t\t}
|
|
134
|
+
\t\t\t});
|
|
135
|
+
\t\t</script>
|
|
136
|
+
\t</div>
|
|
119
137
|
</div>
|
|
120
138
|
HTML
|
|
121
139
|
end
|
|
@@ -144,6 +162,32 @@ class DecisionsOverview < BaseDocument # rubocop:disable Style/Documentation,Met
|
|
|
144
162
|
}
|
|
145
163
|
end
|
|
146
164
|
|
|
165
|
+
# Tally each decision record under its current status (the "*"-marked Status
|
|
166
|
+
# row). Records whose current status is undefined fall under "Undefined", which
|
|
167
|
+
# is ordered last; real statuses keep first-seen order. The count is baked into
|
|
168
|
+
# each label so small categories stay legible on a linear axis next to a
|
|
169
|
+
# dominant one.
|
|
170
|
+
def status_distribution_chart_data # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
171
|
+
counts = Hash.new(0)
|
|
172
|
+
keys = []
|
|
173
|
+
@project.project_data.decisions.each do |doc|
|
|
174
|
+
status = doc.current_status
|
|
175
|
+
key = status.nil? || status.strip.empty? ? UNDEFINED_STATUS_LABEL : status
|
|
176
|
+
keys << key unless counts.key?(key)
|
|
177
|
+
counts[key] += 1
|
|
178
|
+
end
|
|
179
|
+
keys << UNDEFINED_STATUS_LABEL if keys.delete(UNDEFINED_STATUS_LABEL)
|
|
180
|
+
|
|
181
|
+
colors = keys.each_with_index.map do |k, i|
|
|
182
|
+
k == UNDEFINED_STATUS_LABEL ? palette_rgba(CHART_PALETTE.length - 1, 0.5) : palette_rgba(i, 0.5)
|
|
183
|
+
end
|
|
184
|
+
{
|
|
185
|
+
labels: keys.map { |k| "#{k} (#{counts[k]})" },
|
|
186
|
+
datasets: [{ label: 'Decision records', data: keys.map { |k| counts[k] },
|
|
187
|
+
backgroundColor: colors, borderWidth: 0 }]
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
147
191
|
def palette_rgba(index, alpha)
|
|
148
192
|
r, g, b = CHART_PALETTE[index % CHART_PALETTE.length]
|
|
149
193
|
"rgba(#{r}, #{g}, #{b}, #{alpha})"
|
|
@@ -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|
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
key = doc.id.to_s.downcase
|
|
21
|
+
if @by_id.key?(key) && !@by_id[key].equal?(doc)
|
|
22
|
+
@collisions << key
|
|
23
|
+
else
|
|
24
|
+
@by_id[key] = doc
|
|
25
|
+
end
|
|
26
|
+
return unless doc.respond_to?(:path) && doc.path
|
|
27
|
+
|
|
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
|
+
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, \
|
|
@@ -154,6 +154,11 @@ a:active {
|
|
|
154
154
|
text-decoration: none;
|
|
155
155
|
display: inline-block;
|
|
156
156
|
}
|
|
157
|
+
a.broken_link, a.broken_link:link, a.broken_link:visited, span.broken_link {
|
|
158
|
+
color: #c00;
|
|
159
|
+
text-decoration: underline wavy #c00;
|
|
160
|
+
cursor: help;
|
|
161
|
+
}
|
|
157
162
|
div.blockquote {
|
|
158
163
|
display: block;
|
|
159
164
|
background:#f9f9fb;
|
|
@@ -1,5 +1,5 @@
|
|
|
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
3
|
|
|
4
4
|
// Create DB
|
|
5
5
|
const db = await create({
|
|
@@ -19,7 +19,7 @@ const data_rows = await response.json();
|
|
|
19
19
|
let i = 0;
|
|
20
20
|
while (i < data_rows.length) {
|
|
21
21
|
await insert(db, {
|
|
22
|
-
|
|
22
|
+
doc_title: data_rows[i]["doc_title"],
|
|
23
23
|
doc_color: data_rows[i]["doc_color"],
|
|
24
24
|
text: data_rows[i]["text"],
|
|
25
25
|
heading_url: data_rows[i]["heading_url"],
|
|
@@ -66,7 +66,7 @@ async function search_onKeyUp(){
|
|
|
66
66
|
}else{
|
|
67
67
|
|
|
68
68
|
searchResult.hits.forEach ((value, index, array) =>{
|
|
69
|
-
const doc_title = value.document["
|
|
69
|
+
const doc_title = value.document["doc_title"]
|
|
70
70
|
const doc_color = value.document["doc_color"]
|
|
71
71
|
const heading_url = value.document["heading_url"]
|
|
72
72
|
const heading_text = value.document["heading_text"]
|
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.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleksandr Ivanov
|
|
@@ -58,6 +58,7 @@ extra_rdoc_files: []
|
|
|
58
58
|
files:
|
|
59
59
|
- bin/almirah
|
|
60
60
|
- lib/almirah.rb
|
|
61
|
+
- lib/almirah/console_reporter.rb
|
|
61
62
|
- lib/almirah/doc_fabric.rb
|
|
62
63
|
- lib/almirah/doc_items/blockquote.rb
|
|
63
64
|
- lib/almirah/doc_items/code_block.rb
|
|
@@ -89,6 +90,7 @@ files:
|
|
|
89
90
|
- lib/almirah/doc_types/traceability.rb
|
|
90
91
|
- lib/almirah/dom/doc_section.rb
|
|
91
92
|
- lib/almirah/dom/document.rb
|
|
93
|
+
- lib/almirah/link_registry.rb
|
|
92
94
|
- lib/almirah/navigation_pane.rb
|
|
93
95
|
- lib/almirah/project.rb
|
|
94
96
|
- lib/almirah/project/doc_linker.rb
|
|
@@ -96,6 +98,7 @@ files:
|
|
|
96
98
|
- lib/almirah/project_configuration.rb
|
|
97
99
|
- lib/almirah/project_template.rb
|
|
98
100
|
- lib/almirah/project_utility.rb
|
|
101
|
+
- lib/almirah/relative_url.rb
|
|
99
102
|
- lib/almirah/search/specifications_db.rb
|
|
100
103
|
- lib/almirah/source_file_parser.rb
|
|
101
104
|
- lib/almirah/templates/css/main.css
|