Path:
strictdoc/export/html/html_generator.py
Lines:
857
Non-empty lines:
768
Non-empty lines covered with requirements:
768 / 768 (100.0%)
Functions:
16
Functions covered by requirements:
16 / 16 (100.0%)
1
import importlib
2
import os
3
import sys
4
from collections import defaultdict
5
from functools import partial
6
from pathlib import Path
7
from typing import Any, Dict, List, Optional, Set, Tuple
8
9
import orjson
10
from html2pdf4doc import PATH_TO_HTML2PDF4DOC_JS
11
12
from strictdoc.backend.sdoc.models.document import SDocDocument
13
from strictdoc.core.asset_manager import AssetDir
14
from strictdoc.core.document_meta import DocumentMeta
15
from strictdoc.core.file_system.source_tree import SourceTree
16
from strictdoc.core.project_config import ProjectConfig, ProjectFeature
17
from strictdoc.core.traceability_index import TraceabilityIndex
18
from strictdoc.export.html.document_type import DocumentType
19
from strictdoc.export.html.generators.document import DocumentHTMLGenerator
20
from strictdoc.export.html.generators.document_table import (
21
DocumentTableHTMLGenerator,
22
)23
from strictdoc.export.html.generators.document_trace import (
24
DocumentTraceHTMLGenerator,
25
)26
from strictdoc.export.html.html_templates import HTMLTemplates
27
from strictdoc.export.html.renderers.link_renderer import LinkRenderer
28
from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer
29
from strictdoc.features.deep_trace.generator import (
30
DocumentDeepTraceHTMLGenerator,
31
)32
from strictdoc.features.html2pdf.generator import (
33
DocumentHTML2PDFGenerator,
34
)35
from strictdoc.features.project_index.generator import (
36
DocumentTreeHTMLGenerator,
37
)38
from strictdoc.features.project_index.project_map_generator import (
39
ProjectMapGenerator,
40
)41
from strictdoc.features.project_statistics.generator import (
42
ProgressStatisticsGenerator,
43
)44
from strictdoc.features.source_coverage.generator import (
45
SourceFileCoverageHTMLGenerator,
46
)47
from strictdoc.features.source_file_view.generator import (
48
SourceFileViewHTMLGenerator,
49
)50
from strictdoc.features.traceability_matrix.generator import (
51
TraceabilityMatrixHTMLGenerator,
52
)53
from strictdoc.features.tree_map.generator import TreeMapGenerator
54
from strictdoc.helpers.cast import assert_cast
55
from strictdoc.helpers.exception import StrictDocException
56
from strictdoc.helpers.file_modification_time import get_file_modification_time
57
from strictdoc.helpers.file_system import sync_dir
58
from strictdoc.helpers.git_client import GitClient
59
from strictdoc.helpers.mid import MID
60
from strictdoc.helpers.parallelizer import Parallelizer
61
from strictdoc.helpers.paths import SDocRelativePath, path_to_posix_path
62
from strictdoc.helpers.timing import measure_performance, timing_decorator
63
64
65
class HTMLGenerator:
66
def __init__(
67
self, project_config: ProjectConfig, html_templates: HTMLTemplates
68
):69
self.project_config: ProjectConfig = project_config
70
self.html_templates = html_templates
71
self.git_client: GitClient = GitClient()
72
73
def export_complete_tree(
74
self,
75
*,
76
traceability_index: TraceabilityIndex,
77
parallelizer: Parallelizer,
78
) -> None:
79
Path(self.project_config.export_output_html_root).mkdir(
80
parents=True, exist_ok=True
81
)82
83
# Export assets.84
HTMLGenerator.export_assets(
85
traceability_index=traceability_index,
86
project_config=self.project_config,
87
export_output_html_root=self.project_config.export_output_html_root,
88
)89
90
# Export static search index.91
self.export_static_html_search_index(
92
traceability_index=traceability_index
93
)94
95
# Export all documents in parallel.96
export_binding = partial(
97
self.export_single_document_with_performance,
98
traceability_index=traceability_index,
99
)100
101
# By default, do not export included documents. Only, if the option to102
# include is provided.103
documents_to_export: List[SDocDocument] = []
104
105
if self.project_config.export_included_documents:
106
documents_to_export[:] = (
107
traceability_index.document_tree.document_list
108
)109
else:
110
for document_ in traceability_index.document_tree.document_list:
111
if document_.document_is_included():
112
continue113
114
document_meta = assert_cast(document_.meta, DocumentMeta)
115
116
input_doc_full_path = document_meta.input_doc_full_path
117
output_doc_full_path = document_meta.output_document_full_path
118
119
if os.path.isfile(output_doc_full_path) and (
120
get_file_modification_time(input_doc_full_path)
121
< get_file_modification_time(output_doc_full_path)
122
and not traceability_index.file_dependency_manager.must_generate(
123
document_meta.output_document_full_path
124
)125
):126
with measure_performance(f"Skip: {document_.title}"):
127
continue128
129
documents_to_export.append(document_)
130
131
if len(documents_to_export) > 0:
132
if len(traceability_index.document_tree.document_list) <= 25:
133
parallelizer.run_parallel(documents_to_export, export_binding)
134
else:
135
print( # noqa: T201
136
"NOTE: Running document export without parallelization "137
"because the document tree contains more than 25 documents."138
)139
for document_ in documents_to_export:
140
export_binding(document_)
141
142
# Export document tree.143
# FIXME: It is important that this export is **after** the parallelized144
# export of single documents. It turns out that Jinja does not play145
# well with the multiprocessing's processed-based parallelization.146
# _pickle.PicklingError: Can't pickle <function sync_do_first at 0x1077bdf80>: it's not the same object as jinja2.filters.sync_do_first.147
self.export_project_tree_screen(traceability_index=traceability_index)
148
149
# Export JavaScript map of the document tree (project map)150
self.export_project_map(traceability_index=traceability_index)
151
152
if self.project_config.is_activated_tree_map():
153
self.export_tree_map_screen(traceability_index)
154
155
# Export project statistics.156
if self.project_config.is_feature_activated(
157
ProjectFeature.PROJECT_STATISTICS_SCREEN
158
):159
self.export_project_statistics(traceability_index)
160
161
# Export requirements coverage.162
if self.project_config.is_feature_activated(
163
ProjectFeature.TRACEABILITY_MATRIX_SCREEN
164
):165
self.export_requirements_coverage_screen(
166
traceability_index=traceability_index,
167
)168
169
# Export source coverage.170
if self.project_config.is_feature_activated(
171
ProjectFeature.REQUIREMENT_TO_SOURCE_TRACEABILITY
172
):173
self.export_source_files_screens(
174
traceability_index=traceability_index,
175
)176
self.export_source_coverage_screen(
177
traceability_index=traceability_index,
178
)179
180
print( # noqa: T201
181
"Export completed. Documentation tree can be found at:\n"
182
f"{self.project_config.export_output_html_root}"
183
)184
185
@staticmethod186
def export_assets(
187
*,
188
traceability_index: Optional[TraceabilityIndex],
189
project_config: ProjectConfig,
190
export_output_html_root: str,
191
flat_assets: bool = False,
192
) -> None:
193
"""
194
Copy all assets to output dir during HTML/PDF generation.195
196
:param bool flat_assets: This parameter is always set to False except when197
exporting a "bundle document" with HTML2PDF.198
The bundle document contains all documents of199
the documentation tree. In this case, all assets200
are simply copied to the top level _assets folder,201
independently on how nested the contained documents are.202
"""203
204
# Export StrictDoc's own assets.205
output_html_static_files = os.path.join(
206
export_output_html_root,
207
project_config.dir_for_sdoc_assets,
208
)209
for static_files_path in project_config.get_static_files_paths():
210
sync_dir(
211
static_files_path,
212
output_html_static_files,
213
message="Copying StrictDoc's assets",
214
)215
216
# Export MathJax.217
if project_config.is_feature_activated(ProjectFeature.MATHJAX):
218
output_html_mathjax = os.path.join(
219
export_output_html_root,
220
project_config.dir_for_sdoc_assets,
221
"mathjax",
222
)223
Path(output_html_mathjax).mkdir(parents=True, exist_ok=True)
224
mathjax_src = os.path.join(
225
project_config.get_extra_static_files_path(), "mathjax"
226
)227
sync_dir(
228
mathjax_src,
229
output_html_mathjax,
230
message="Copying MathJax assets",
231
)232
233
# Export Mermaid.234
if project_config.is_feature_activated(ProjectFeature.MERMAID):
235
output_html_mathjax = os.path.join(
236
export_output_html_root,
237
project_config.dir_for_sdoc_assets,
238
"mermaid",
239
)240
Path(output_html_mathjax).mkdir(parents=True, exist_ok=True)
241
mermaid_src = os.path.join(
242
project_config.get_extra_static_files_path(), "mermaid"
243
)244
sync_dir(
245
mermaid_src,
246
output_html_mathjax,
247
message="Copying Mermaid assets",
248
)249
250
# Export Rapidoc.251
if project_config.is_feature_activated(ProjectFeature.RAPIDOC):
252
output_html_rapidoc = os.path.join(
253
export_output_html_root,
254
project_config.dir_for_sdoc_assets,
255
"rapidoc",
256
)257
Path(output_html_rapidoc).mkdir(parents=True, exist_ok=True)
258
rapidoc_src = os.path.join(
259
project_config.get_extra_static_files_path(), "rapidoc"
260
)261
sync_dir(
262
rapidoc_src,
263
output_html_rapidoc,
264
message="Copying Rapidoc assets",
265
)266
267
# Export HTML2PDF.268
if project_config.is_feature_activated(ProjectFeature.HTML2PDF):
269
sync_dir(
270
os.path.dirname(PATH_TO_HTML2PDF4DOC_JS),
271
output_html_static_files,
272
message="Copying HTML2PDF.js",
273
)274
275
# Export custom html2pdf template.276
if project_config.html2pdf_template is not None:
277
output_custom_html2pdf_template = os.path.join(
278
export_output_html_root,
279
project_config.dir_for_sdoc_assets,
280
"html2pdf_template",
281
)282
sync_dir(
283
os.path.abspath(
284
os.path.dirname(project_config.html2pdf_template)
285
),286
output_custom_html2pdf_template,
287
message="Copying Custom HTML2PDF template assets",
288
)289
290
# Export project's assets.291
292
if traceability_index is not None:
293
redundant_assets: Dict[str, List[SDocRelativePath]] = {}
294
for document_ in traceability_index.document_tree.document_list:
295
assert document_.meta is not None
296
for (
297
included_document_298
) in document_.iterate_included_documents_depth_first():
299
assert included_document_.meta is not None
300
301
redundant_assets.setdefault(
302
document_.meta.input_doc_assets_dir_rel_path.relative_path_posix,
303
[],304
)305
redundant_assets[
306
document_.meta.input_doc_assets_dir_rel_path.relative_path_posix
307
].append(
308
included_document_.meta.input_doc_assets_dir_rel_path
309
)310
311
assert traceability_index.asset_manager is not None
312
313
asset_dir_: AssetDir
314
for asset_dir_ in traceability_index.asset_manager.iterate():
315
source_path = asset_dir_.full_path
316
output_relative_path = asset_dir_.relative_path
317
318
destination_path = os.path.join(
319
export_output_html_root,
320
output_relative_path.relative_path
321
if not flat_assets
322
else "_assets",
323
)324
325
sync_dir(
326
source_path,
327
destination_path,
328
message=f'Copying project assets "{output_relative_path.relative_path}"',
329
)330
redundant_asset_paths = redundant_assets.get(
331
output_relative_path.relative_path_posix
332
)333
if redundant_asset_paths is not None:
334
for redundant_asset_ in redundant_asset_paths:
335
destination_path = os.path.join(
336
export_output_html_root,
337
redundant_asset_.relative_path
338
if not flat_assets
339
else "_assets",
340
)341
sync_dir(
342
source_path,
343
destination_path,
344
message=f'Copying project assets "{output_relative_path.relative_path}"',
345
)346
347
def export_single_document_with_performance(
348
self,
349
document: SDocDocument,
350
traceability_index: TraceabilityIndex,
351
specific_documents: Optional[Tuple[DocumentType, ...]] = None,
352
) -> None:
353
if specific_documents is None:
354
specific_documents = DocumentType.all()
355
356
with measure_performance(f"Published: {document.title}"):
357
self.export_single_document(
358
document,
359
traceability_index,
360
specific_documents=specific_documents,
361
)362
363
def export_single_document(
364
self,
365
document: SDocDocument,
366
traceability_index: TraceabilityIndex,
367
specific_documents: Optional[Tuple[DocumentType, ...]] = None,
368
) -> SDocDocument:
369
if document.config.layout == "Website":
370
specific_documents = (DocumentType.DOCUMENT,)
371
elif specific_documents is None:
372
specific_documents = DocumentType.all()
373
374
assert document.meta is not None
375
376
document_meta: DocumentMeta = document.meta
377
378
document_output_folder = document_meta.output_document_dir_full_path
379
Path(document_output_folder).mkdir(parents=True, exist_ok=True)
380
381
root_path = document.meta.get_root_path_prefix()
382
link_renderer = LinkRenderer(
383
root_path=root_path,
384
static_path=self.project_config.dir_for_sdoc_assets,
385
)386
markup_renderer = MarkupRenderer.create(
387
document.config.markup,
388
traceability_index,
389
link_renderer,
390
self.html_templates,
391
self.project_config,
392
document,
393
)394
395
if DocumentType.DOCUMENT in specific_documents:
396
# Single Document pages.397
document_content = DocumentHTMLGenerator.export(
398
self.project_config,
399
document,
400
traceability_index,
401
markup_renderer,
402
link_renderer,
403
git_client=self.git_client,
404
html_templates=self.html_templates,
405
)406
document_out_file = document_meta.get_html_doc_path()
407
with open(document_out_file, "w", encoding="utf8") as file:
408
file.write(document_content)
409
410
# Single Document Table pages.411
if (
412
self.project_config.is_feature_activated(
413
ProjectFeature.TABLE_SCREEN
414
)415
and DocumentType.TABLE in specific_documents
416
):417
document_content = DocumentTableHTMLGenerator.export(
418
self.project_config,
419
document,
420
traceability_index,
421
markup_renderer,
422
link_renderer,
423
git_client=self.git_client,
424
html_templates=self.html_templates,
425
)426
document_out_file = document_meta.get_html_table_path()
427
with open(document_out_file, "w", encoding="utf8") as file:
428
file.write(document_content)
429
430
# Single Document Traceability pages.431
if (
432
self.project_config.is_feature_activated(
433
ProjectFeature.TRACEABILITY_SCREEN
434
)435
and DocumentType.TRACE in specific_documents
436
):437
document_content = DocumentTraceHTMLGenerator.export(
438
self.project_config,
439
document,
440
traceability_index,
441
markup_renderer,
442
link_renderer,
443
git_client=self.git_client,
444
html_templates=self.html_templates,
445
)446
document_out_file = document_meta.get_html_traceability_path()
447
with open(document_out_file, "w", encoding="utf8") as file:
448
file.write(document_content)
449
450
# Single Document Deep Traceability pages.451
if (
452
self.project_config.is_feature_activated(
453
ProjectFeature.DEEP_TRACEABILITY_SCREEN
454
)455
and DocumentType.DEEPTRACE in specific_documents
456
):457
document_content = DocumentDeepTraceHTMLGenerator.export_deep(
458
self.project_config,
459
document,
460
traceability_index,
461
markup_renderer,
462
link_renderer,
463
git_client=self.git_client,
464
html_templates=self.html_templates,
465
)466
document_out_file = document_meta.get_html_deep_traceability_path()
467
with open(document_out_file, "w", encoding="utf8") as file:
468
file.write(document_content)
469
470
# Single Document PDF pages.471
if (
472
self.project_config.is_feature_activated(ProjectFeature.HTML2PDF)
473
and DocumentType.PDF in specific_documents
474
):475
document_content = DocumentHTML2PDFGenerator.export(
476
self.project_config,
477
document,
478
traceability_index,
479
markup_renderer,
480
link_renderer,
481
git_client=self.git_client,
482
html_templates=self.html_templates,
483
)484
document_out_file = document_meta.get_html_pdf_path()
485
with open(document_out_file, "w", encoding="utf8") as file:
486
file.write(document_content)
487
488
return document
489
490
def export_project_tree_screen(
491
self,
492
*,
493
traceability_index: TraceabilityIndex,
494
) -> None:
495
Path(self.project_config.export_output_html_root).mkdir(
496
parents=True, exist_ok=True
497
)498
output_file = os.path.join(
499
self.project_config.export_output_html_root, "index.html"
500
)501
writer = DocumentTreeHTMLGenerator()
502
output = writer.export(
503
self.project_config,
504
traceability_index=traceability_index,
505
html_templates=self.html_templates,
506
)507
with open(output_file, "w", encoding="utf8") as file:
508
file.write(output)
509
510
def export_project_map(
511
self,
512
*,
513
traceability_index: TraceabilityIndex,
514
) -> None:
515
assets_dir = os.path.join(
516
self.project_config.export_output_html_root,
517
self.project_config.dir_for_sdoc_assets,
518
)519
output_file = os.path.join(assets_dir, "project_map.js")
520
writer = ProjectMapGenerator()
521
output = writer.export(
522
self.project_config,
523
traceability_index=traceability_index,
524
html_templates=self.html_templates,
525
)526
with open(output_file, "w", encoding="utf8") as file:
527
file.write(output)
528
529
def export_requirements_coverage_screen(
530
self,
531
*,
532
traceability_index: TraceabilityIndex,
533
) -> None:
534
requirements_coverage_content = TraceabilityMatrixHTMLGenerator.export(
535
project_config=self.project_config,
536
traceability_index=traceability_index,
537
html_templates=self.html_templates,
538
)539
output_html_requirements_coverage = os.path.join(
540
self.project_config.export_output_html_root,
541
"traceability_matrix.html",
542
)543
with open(
544
output_html_requirements_coverage, "w", encoding="utf8"
545
) as file:
546
file.write(requirements_coverage_content)
547
548
@timing_decorator("Export source file pages")
549
def export_source_files_screens(
550
self,
551
*,
552
traceability_index: TraceabilityIndex,
553
) -> None:
554
assert isinstance(
555
traceability_index.document_tree.source_tree, SourceTree
556
), traceability_index.document_tree.source_tree
557
print("Generating source files:") # noqa: T201
558
for (
559
source_file560
) in traceability_index.document_tree.source_tree.source_files:
561
if not source_file.is_referenced:
562
continue563
564
SourceFileViewHTMLGenerator.export_to_file(
565
project_config=self.project_config,
566
source_file=source_file,
567
traceability_index=traceability_index,
568
html_templates=self.html_templates,
569
)570
571
def export_source_coverage_screen(
572
self,
573
*,
574
traceability_index: TraceabilityIndex,
575
) -> None:
576
assert isinstance(
577
traceability_index.document_tree.source_tree, SourceTree
578
), traceability_index.document_tree.source_tree
579
580
source_coverage_content = SourceFileCoverageHTMLGenerator.export(
581
project_config=self.project_config,
582
traceability_index=traceability_index,
583
html_templates=self.html_templates,
584
)585
output_html_source_coverage = os.path.join(
586
self.project_config.export_output_html_root, "source_coverage.html"
587
)588
with open(output_html_source_coverage, "w", encoding="utf8") as file:
589
file.write(source_coverage_content)
590
591
def export_single_source_file_screen(
592
self,
593
*,
594
traceability_index: TraceabilityIndex,
595
path_to_source_file: str,
596
) -> None:
597
assert isinstance(
598
traceability_index.document_tree.source_tree, SourceTree
599
), traceability_index.document_tree.source_tree
600
601
# FIXME: path_to_source_file must not enter this function with forward slashes.602
# Test and fix this on Windows.603
# https://github.com/strictdoc-project/strictdoc/issues/2068604
relative_path_to_source_file = path_to_posix_path(path_to_source_file)
605
relative_path_to_source_file = (
606
relative_path_to_source_file.removeprefix("_source_files/")
607
)608
relative_path_to_source_file = (
609
relative_path_to_source_file.removesuffix(".html")
610
)611
612
for (
613
source_file614
) in traceability_index.document_tree.source_tree.source_files:
615
if not source_file.is_referenced:
616
continue617
618
if (
619
relative_path_to_source_file620
== source_file.in_doctree_source_file_rel_path_posix
621
):622
SourceFileViewHTMLGenerator.export_to_file(
623
project_config=self.project_config,
624
source_file=source_file,
625
traceability_index=traceability_index,
626
html_templates=self.html_templates,
627
)628
return629
630
raise FileNotFoundError
631
- "6.8.1. Display project statistics" (REQUIREMENT)
- "6.8.2. Support for user-provided custom statistics generators" (REQUIREMENT)
632
def export_project_statistics(
633
self,
634
traceability_index: TraceabilityIndex,
635
) -> None:
636
"""
637
Export project statistics to a dedicated HTML page.638
639
@relation(SDOC-SRS-97, scope=function)640
@relation(SDOC-SRS-154, scope=function)641
"""642
643
link_renderer = LinkRenderer(
644
root_path="",
645
static_path=self.project_config.dir_for_sdoc_assets,
646
)647
648
statistics_generator = ProgressStatisticsGenerator
649
650
if (
651
custom_statistics_generator_path652
:= self.project_config.statistics_generator
653
) is not None:
654
# It is important to add the input folder to the import path.655
# Otherwise, the custom statistics generator may not be found.656
# In fact, a more reasonable path to add would be the project config657
# path, but since it is not maintained by ProjectConfig yet and658
# usually equals the input path, add the input path for659
# now.660
input_paths = self.project_config.input_paths
661
assert input_paths is not None and len(input_paths) > 0, (
662
"Expected a valid input path."663
)664
sys.path.insert(0, input_paths[0])
665
666
module_path, class_name = custom_statistics_generator_path.rsplit(
667
".", 1
668
)669
try:
670
module = importlib.import_module(module_path)
671
statistics_generator = getattr(module, class_name)
672
except ModuleNotFoundError as module_not_found_error_:
673
raise StrictDocException(
674
"Could not import a user-provided statistics generator: "675
f"{module_not_found_error_}."
676
) from module_not_found_error_
677
678
document_content = statistics_generator.export(
679
self.project_config,
680
traceability_index,
681
link_renderer,
682
html_templates=self.html_templates,
683
)684
output_html_source_coverage = os.path.join(
685
self.project_config.export_output_html_root,
686
"project_statistics.html",
687
)688
with open(output_html_source_coverage, "w", encoding="utf8") as file:
689
file.write(document_content)
690
691
@timing_decorator("Export static HTML search index")
- "6.14.1. Content search" (REQUIREMENT)
- "6.1. Static HTML search" (DESIGN)
692
def export_static_html_search_index(
693
self,
694
traceability_index: TraceabilityIndex,
695
*,
696
force_regeneration: bool = False,
697
) -> None:
698
"""
699
Export a static search index as dictionaries in .js files.700
701
@relation(SDOC-SRS-155, scope=function)702
@relation(SDOC-SRS-156, scope=function)703
"""704
705
if not force_regeneration:
706
# First check if there is nothing to do because no documents have707
# been changed or regenerated.708
709
# FIXME: This is wrong. FIX!710
must_regenerate = (
711
len(traceability_index.document_tree.document_list) == 0
712
)713
714
for document_ in traceability_index.document_tree.document_list:
715
assert document_.meta is not None
716
if traceability_index.file_dependency_manager.must_generate(
717
document_.meta.output_document_full_path
718
):719
must_regenerate = True
720
break721
722
if not must_regenerate:
723
print( # noqa: T201
724
"All documents are up-to-date. "725
"Skipping the generation of a search index."726
)727
# If no documents need to be regenerated, set the728
# search_index_timestamp to the timestamp of the first document.729
# The HTML/JS code can rely on this timestamp to decide whether730
# it has to re-read the search index from the JS file or it can731
# fetch it from the DB.732
if len(traceability_index.document_tree.document_list) > 0:
733
first_document = (
734
traceability_index.document_tree.document_list[0]
735
)736
assert first_document.meta is not None
737
traceability_index.search_index_timestamp = (
738
get_file_modification_time(
739
first_document.meta.input_doc_full_path
740
)741
)742
return743
744
if force_regeneration:
745
for document_ in traceability_index.document_tree.document_list:
746
document_.build_search_index()
747
748
global_index: Dict[str, Set[int]] = defaultdict(set)
749
global_map_nodes_by_mid: Dict[int, Dict[str, str]] = {}
750
751
document_index_list: List[Dict[str, Set[str]]] = []
752
document_map_list: List[Dict[int, Dict[str, str]]] = []
753
754
map_mid_to_numbers: Dict[str, int] = {}
755
756
with measure_performance("Build search index"):
757
for document_ in traceability_index.document_tree.document_list:
758
assert document_.meta is not None
759
document_index_list.append(
760
document_.search_index.document_index
761
)762
map_nodes_by_numbers: Dict[int, Dict[str, str]] = {}
763
for (
764
node_mid_,
765
node_dict_,
766
) in document_.search_index.map_nodes_by_mid.items():
767
if node_mid_ not in map_mid_to_numbers:
768
map_mid_to_numbers[node_mid_] = (
769
len(map_mid_to_numbers) + 1
770
)771
document_mid_number = map_mid_to_numbers[node_mid_]
772
assert isinstance(document_mid_number, int)
773
map_nodes_by_numbers[document_mid_number] = node_dict_
774
775
document_map_list.append(map_nodes_by_numbers)
776
for document_index_ in document_index_list:
777
for term_, document_mids_ in document_index_.items():
778
document_mid_numbers = set()
779
for document_mid_ in document_mids_:
780
document_mid_number = map_mid_to_numbers[document_mid_]
781
document_mid_numbers.add(document_mid_number)
782
global_index[term_].update(document_mid_numbers)
783
for map_nodes_by_mid_ in document_map_list:
784
global_map_nodes_by_mid.update(map_nodes_by_mid_)
785
786
link_renderer = LinkRenderer(
787
root_path="",
788
static_path=self.project_config.dir_for_sdoc_assets,
789
)790
for _, node_ in global_map_nodes_by_mid.items():
791
# When running on server, the MID is used as a link to the node.792
# The MID is then resolved to the correct URL by the server when793
# requested at /UID/{uid_or_mid}.794
# This ensures that all nodes can be reached with MID, including795
# the nodes that don't have a UID.796
if self.project_config.is_running_on_server:
797
node_["_LINK"] = node_["MID"]
798
799
# When running static HTML, the resolution of _LINKs happens through800
# the auto-generated static JS project_map.js that has a format of:801
# {<local anchor>: MID}802
else:
803
node = traceability_index.get_node_by_mid(MID(node_["MID"]))
804
node_["_LINK"] = link_renderer.render_local_anchor(node)
805
806
def default(obj: Any) -> Any:
807
if isinstance(obj, set):
808
return list(obj)
809
raise TypeError
810
811
with measure_performance("Serialize search index to JS"):
812
document_content = (
813
b"window.StrictDoc = window.StrictDoc || {};\n"
814
b"window.StrictDoc.search = window.StrictDoc.search || {};\n"
815
b"window.StrictDoc.search.index = "
816
+ orjson.dumps(
817
global_index,
818
option=orjson.OPT_NON_STR_KEYS,
819
default=default,
820
)821
+ b";\n\n"
822
)823
824
with measure_performance("Serialize lookup map {MID => node} to JS"):
825
document_content += (
826
b"window.StrictDoc.search.nodesByMid = "
827
+ orjson.dumps(
828
global_map_nodes_by_mid, option=orjson.OPT_NON_STR_KEYS
829
)830
+ b";\n"
831
)832
833
# Export StrictDoc's own assets.834
output_html_static_files = os.path.join(
835
self.project_config.export_output_html_root,
836
self.project_config.dir_for_sdoc_assets,
837
)838
output_html_source_coverage = os.path.join(
839
output_html_static_files,
840
"static_html_search_index.js",
841
)842
with open(output_html_source_coverage, "wb") as file:
843
file.write(document_content)
844
845
traceability_index.search_index_timestamp = get_file_modification_time(
846
output_html_source_coverage847
)848
849
def export_tree_map_screen(
850
self,
851
traceability_index: TraceabilityIndex,
852
) -> None:
853
TreeMapGenerator.export(
854
project_config=self.project_config,
855
traceability_index=traceability_index,
856
html_templates=self.html_templates,
857
)