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.
@@ -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
- # Calculate the relative path depth to determine correct number of parent directory symbols
82
- relative_path = @path.sub("#{@root_path}/", '')
83
- depth = relative_path.count('/') + 1 # +1 for the repository folder
84
- depth += 1 # for the source_files folder
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.get_section_info
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.get_section_info
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.get_section_info}</a>\n"
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|
@@ -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.get_section_info}</a>\n"
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
@@ -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) # rubocop:disable Metrics/MethodLength
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
@@ -4,7 +4,7 @@ class ProjectConfiguration
4
4
  attr_accessor :project_root_directory, :parameters
5
5
 
6
6
  def initialize(path)
7
- @project_root_directory = path
7
+ @project_root_directory = File.expand_path(path)
8
8
  @parameters = {}
9
9
  load_project_file
10
10
  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-002')
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 = { 'document' => i.parent_doc.title, \
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 = { 'document' => item_for_reference.parent_doc.title, \
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 = { 'document' => item_for_reference.parent_doc.title, \
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 = { 'document' => item_for_reference.parent_doc.title, \
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
- height: 100%; /* 100% Full-height */
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
- captionText.innerHTML = clicked.alt;
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@latest/dist/index.js'
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
- document: data_rows[i]["document"],
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["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
- i.style.backgroundColor = "#" + doc_color;
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)