Path:
strictdoc/export/html/html_generator.py
Lines:
873
Non-empty lines:
783
Non-empty lines covered with requirements:
783 / 783 (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_deep_trace import (
21
DocumentDeepTraceHTMLGenerator,
22
)23
from strictdoc.export.html.generators.document_table import (
24
DocumentTableHTMLGenerator,
25
)26
from strictdoc.export.html.generators.document_trace import (
27
DocumentTraceHTMLGenerator,
28
)29
from strictdoc.export.html.generators.document_tree import (
30
DocumentTreeHTMLGenerator,
31
)32
from strictdoc.export.html.generators.project_map import (
33
ProjectMapGenerator,
34
)35
from strictdoc.export.html.generators.source_file_coverage import (
36
SourceFileCoverageHTMLGenerator,
37
)38
from strictdoc.export.html.generators.source_file_view_generator import (
39
SourceFileViewHTMLGenerator,
40
)41
from strictdoc.export.html.generators.traceability_matrix import (
42
TraceabilityMatrixHTMLGenerator,
43
)44
from strictdoc.export.html.html_templates import HTMLTemplates
45
from strictdoc.export.html.renderers.link_renderer import LinkRenderer
46
from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer
47
from strictdoc.features.html2pdf.generator import (
48
DocumentHTML2PDFGenerator,
49
)50
from strictdoc.features.project_statistics.generator import (
51
ProgressStatisticsGenerator,
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
sync_dir(
210
project_config.get_static_files_path(),
211
output_html_static_files,
212
message="Copying StrictDoc's assets",
213
)214
215
# Export MathJax.216
if project_config.is_feature_activated(ProjectFeature.MATHJAX):
217
output_html_mathjax = os.path.join(
218
export_output_html_root,
219
project_config.dir_for_sdoc_assets,
220
"mathjax",
221
)222
Path(output_html_mathjax).mkdir(parents=True, exist_ok=True)
223
mathjax_src = os.path.join(
224
project_config.get_extra_static_files_path(), "mathjax"
225
)226
sync_dir(
227
mathjax_src,
228
output_html_mathjax,
229
message="Copying MathJax assets",
230
)231
232
# Export Mermaid.233
if project_config.is_feature_activated(ProjectFeature.MERMAID):
234
output_html_mathjax = os.path.join(
235
export_output_html_root,
236
project_config.dir_for_sdoc_assets,
237
"mermaid",
238
)239
Path(output_html_mathjax).mkdir(parents=True, exist_ok=True)
240
mermaid_src = os.path.join(
241
project_config.get_extra_static_files_path(), "mermaid"
242
)243
sync_dir(
244
mermaid_src,
245
output_html_mathjax,
246
message="Copying Mermaid assets",
247
)248
249
# Export Rapidoc.250
if project_config.is_feature_activated(ProjectFeature.RAPIDOC):
251
output_html_rapidoc = os.path.join(
252
export_output_html_root,
253
project_config.dir_for_sdoc_assets,
254
"rapidoc",
255
)256
Path(output_html_rapidoc).mkdir(parents=True, exist_ok=True)
257
rapidoc_src = os.path.join(
258
project_config.get_extra_static_files_path(), "rapidoc"
259
)260
sync_dir(
261
rapidoc_src,
262
output_html_rapidoc,
263
message="Copying Rapidoc assets",
264
)265
266
# Export NESTOR.267
if project_config.is_feature_activated(ProjectFeature.NESTOR):
268
output_html_nestor = os.path.join(
269
export_output_html_root,
270
project_config.dir_for_sdoc_assets,
271
"nestor",
272
)273
Path(output_html_nestor).mkdir(parents=True, exist_ok=True)
274
mathjax_src = os.path.join(
275
project_config.get_extra_static_files_path(), "nestor"
276
)277
sync_dir(
278
mathjax_src,
279
output_html_nestor,
280
message="Copying Nestor assets",
281
)282
283
# Export HTML2PDF.284
if project_config.is_feature_activated(ProjectFeature.HTML2PDF):
285
sync_dir(
286
os.path.dirname(PATH_TO_HTML2PDF4DOC_JS),
287
output_html_static_files,
288
message="Copying HTML2PDF.js",
289
)290
291
# Export custom html2pdf template.292
if project_config.html2pdf_template is not None:
293
output_custom_html2pdf_template = os.path.join(
294
export_output_html_root,
295
project_config.dir_for_sdoc_assets,
296
"html2pdf_template",
297
)298
sync_dir(
299
os.path.abspath(
300
os.path.dirname(project_config.html2pdf_template)
301
),302
output_custom_html2pdf_template,
303
message="Copying Custom HTML2PDF template assets",
304
)305
306
# Export project's assets.307
308
if traceability_index is not None:
309
redundant_assets: Dict[str, List[SDocRelativePath]] = {}
310
for document_ in traceability_index.document_tree.document_list:
311
assert document_.meta is not None
312
for (
313
included_document_314
) in document_.iterate_included_documents_depth_first():
315
assert included_document_.meta is not None
316
317
redundant_assets.setdefault(
318
document_.meta.input_doc_assets_dir_rel_path.relative_path_posix,
319
[],320
)321
redundant_assets[
322
document_.meta.input_doc_assets_dir_rel_path.relative_path_posix
323
].append(
324
included_document_.meta.input_doc_assets_dir_rel_path
325
)326
327
assert traceability_index.asset_manager is not None
328
329
asset_dir_: AssetDir
330
for asset_dir_ in traceability_index.asset_manager.iterate():
331
source_path = asset_dir_.full_path
332
output_relative_path = asset_dir_.relative_path
333
334
destination_path = os.path.join(
335
export_output_html_root,
336
output_relative_path.relative_path
337
if not flat_assets
338
else "_assets",
339
)340
341
sync_dir(
342
source_path,
343
destination_path,
344
message=f'Copying project assets "{output_relative_path.relative_path}"',
345
)346
redundant_asset_paths = redundant_assets.get(
347
output_relative_path.relative_path_posix
348
)349
if redundant_asset_paths is not None:
350
for redundant_asset_ in redundant_asset_paths:
351
destination_path = os.path.join(
352
export_output_html_root,
353
redundant_asset_.relative_path
354
if not flat_assets
355
else "_assets",
356
)357
sync_dir(
358
source_path,
359
destination_path,
360
message=f'Copying project assets "{output_relative_path.relative_path}"',
361
)362
363
def export_single_document_with_performance(
364
self,
365
document: SDocDocument,
366
traceability_index: TraceabilityIndex,
367
specific_documents: Optional[Tuple[DocumentType, ...]] = None,
368
) -> None:
369
if specific_documents is None:
370
specific_documents = DocumentType.all()
371
372
with measure_performance(f"Published: {document.title}"):
373
self.export_single_document(
374
document,
375
traceability_index,
376
specific_documents=specific_documents,
377
)378
379
def export_single_document(
380
self,
381
document: SDocDocument,
382
traceability_index: TraceabilityIndex,
383
specific_documents: Optional[Tuple[DocumentType, ...]] = None,
384
) -> SDocDocument:
385
if document.config.layout == "Website":
386
specific_documents = (DocumentType.DOCUMENT,)
387
elif specific_documents is None:
388
specific_documents = DocumentType.all()
389
390
assert document.meta is not None
391
392
document_meta: DocumentMeta = document.meta
393
394
document_output_folder = document_meta.output_document_dir_full_path
395
Path(document_output_folder).mkdir(parents=True, exist_ok=True)
396
397
root_path = document.meta.get_root_path_prefix()
398
link_renderer = LinkRenderer(
399
root_path=root_path,
400
static_path=self.project_config.dir_for_sdoc_assets,
401
)402
markup_renderer = MarkupRenderer.create(
403
document.config.markup,
404
traceability_index,
405
link_renderer,
406
self.html_templates,
407
self.project_config,
408
document,
409
)410
411
if DocumentType.DOCUMENT in specific_documents:
412
# Single Document pages.413
document_content = DocumentHTMLGenerator.export(
414
self.project_config,
415
document,
416
traceability_index,
417
markup_renderer,
418
link_renderer,
419
git_client=self.git_client,
420
html_templates=self.html_templates,
421
)422
document_out_file = document_meta.get_html_doc_path()
423
with open(document_out_file, "w", encoding="utf8") as file:
424
file.write(document_content)
425
426
# Single Document Table pages.427
if (
428
self.project_config.is_feature_activated(
429
ProjectFeature.TABLE_SCREEN
430
)431
and DocumentType.TABLE in specific_documents
432
):433
document_content = DocumentTableHTMLGenerator.export(
434
self.project_config,
435
document,
436
traceability_index,
437
markup_renderer,
438
link_renderer,
439
git_client=self.git_client,
440
html_templates=self.html_templates,
441
)442
document_out_file = document_meta.get_html_table_path()
443
with open(document_out_file, "w", encoding="utf8") as file:
444
file.write(document_content)
445
446
# Single Document Traceability pages.447
if (
448
self.project_config.is_feature_activated(
449
ProjectFeature.TRACEABILITY_SCREEN
450
)451
and DocumentType.TRACE in specific_documents
452
):453
document_content = DocumentTraceHTMLGenerator.export(
454
self.project_config,
455
document,
456
traceability_index,
457
markup_renderer,
458
link_renderer,
459
git_client=self.git_client,
460
html_templates=self.html_templates,
461
)462
document_out_file = document_meta.get_html_traceability_path()
463
with open(document_out_file, "w", encoding="utf8") as file:
464
file.write(document_content)
465
466
# Single Document Deep Traceability pages.467
if (
468
self.project_config.is_feature_activated(
469
ProjectFeature.DEEP_TRACEABILITY_SCREEN
470
)471
and DocumentType.DEEPTRACE in specific_documents
472
):473
document_content = DocumentDeepTraceHTMLGenerator.export_deep(
474
self.project_config,
475
document,
476
traceability_index,
477
markup_renderer,
478
link_renderer,
479
git_client=self.git_client,
480
html_templates=self.html_templates,
481
)482
document_out_file = document_meta.get_html_deep_traceability_path()
483
with open(document_out_file, "w", encoding="utf8") as file:
484
file.write(document_content)
485
486
# Single Document PDF pages.487
if (
488
self.project_config.is_feature_activated(ProjectFeature.HTML2PDF)
489
and DocumentType.PDF in specific_documents
490
):491
document_content = DocumentHTML2PDFGenerator.export(
492
self.project_config,
493
document,
494
traceability_index,
495
markup_renderer,
496
link_renderer,
497
git_client=self.git_client,
498
html_templates=self.html_templates,
499
)500
document_out_file = document_meta.get_html_pdf_path()
501
with open(document_out_file, "w", encoding="utf8") as file:
502
file.write(document_content)
503
504
return document
505
506
def export_project_tree_screen(
507
self,
508
*,
509
traceability_index: TraceabilityIndex,
510
) -> None:
511
Path(self.project_config.export_output_html_root).mkdir(
512
parents=True, exist_ok=True
513
)514
output_file = os.path.join(
515
self.project_config.export_output_html_root, "index.html"
516
)517
writer = DocumentTreeHTMLGenerator()
518
output = writer.export(
519
self.project_config,
520
traceability_index=traceability_index,
521
html_templates=self.html_templates,
522
)523
with open(output_file, "w", encoding="utf8") as file:
524
file.write(output)
525
526
def export_project_map(
527
self,
528
*,
529
traceability_index: TraceabilityIndex,
530
) -> None:
531
assets_dir = os.path.join(
532
self.project_config.export_output_html_root,
533
self.project_config.dir_for_sdoc_assets,
534
)535
output_file = os.path.join(assets_dir, "project_map.js")
536
writer = ProjectMapGenerator()
537
output = writer.export(
538
self.project_config,
539
traceability_index=traceability_index,
540
html_templates=self.html_templates,
541
)542
with open(output_file, "w", encoding="utf8") as file:
543
file.write(output)
544
545
def export_requirements_coverage_screen(
546
self,
547
*,
548
traceability_index: TraceabilityIndex,
549
) -> None:
550
requirements_coverage_content = TraceabilityMatrixHTMLGenerator.export(
551
project_config=self.project_config,
552
traceability_index=traceability_index,
553
html_templates=self.html_templates,
554
)555
output_html_requirements_coverage = os.path.join(
556
self.project_config.export_output_html_root,
557
"traceability_matrix.html",
558
)559
with open(
560
output_html_requirements_coverage, "w", encoding="utf8"
561
) as file:
562
file.write(requirements_coverage_content)
563
564
@timing_decorator("Export source file pages")
565
def export_source_files_screens(
566
self,
567
*,
568
traceability_index: TraceabilityIndex,
569
) -> None:
570
assert isinstance(
571
traceability_index.document_tree.source_tree, SourceTree
572
), traceability_index.document_tree.source_tree
573
print("Generating source files:") # noqa: T201
574
for (
575
source_file576
) in traceability_index.document_tree.source_tree.source_files:
577
if not source_file.is_referenced:
578
continue579
580
SourceFileViewHTMLGenerator.export_to_file(
581
project_config=self.project_config,
582
source_file=source_file,
583
traceability_index=traceability_index,
584
html_templates=self.html_templates,
585
)586
587
def export_source_coverage_screen(
588
self,
589
*,
590
traceability_index: TraceabilityIndex,
591
) -> None:
592
assert isinstance(
593
traceability_index.document_tree.source_tree, SourceTree
594
), traceability_index.document_tree.source_tree
595
596
source_coverage_content = SourceFileCoverageHTMLGenerator.export(
597
project_config=self.project_config,
598
traceability_index=traceability_index,
599
html_templates=self.html_templates,
600
)601
output_html_source_coverage = os.path.join(
602
self.project_config.export_output_html_root, "source_coverage.html"
603
)604
with open(output_html_source_coverage, "w", encoding="utf8") as file:
605
file.write(source_coverage_content)
606
607
def export_single_source_file_screen(
608
self,
609
*,
610
traceability_index: TraceabilityIndex,
611
path_to_source_file: str,
612
) -> None:
613
assert isinstance(
614
traceability_index.document_tree.source_tree, SourceTree
615
), traceability_index.document_tree.source_tree
616
617
# FIXME: path_to_source_file must not enter this function with forward slashes.618
# Test and fix this on Windows.619
# https://github.com/strictdoc-project/strictdoc/issues/2068620
relative_path_to_source_file = path_to_posix_path(path_to_source_file)
621
relative_path_to_source_file = (
622
relative_path_to_source_file.removeprefix("_source_files/")
623
)624
relative_path_to_source_file = (
625
relative_path_to_source_file.removesuffix(".html")
626
)627
628
for (
629
source_file630
) in traceability_index.document_tree.source_tree.source_files:
631
if not source_file.is_referenced:
632
continue633
634
if (
635
relative_path_to_source_file636
== source_file.in_doctree_source_file_rel_path_posix
637
):638
SourceFileViewHTMLGenerator.export_to_file(
639
project_config=self.project_config,
640
source_file=source_file,
641
traceability_index=traceability_index,
642
html_templates=self.html_templates,
643
)644
return645
646
raise FileNotFoundError
647
- "6.8.1. Display project statistics" (REQUIREMENT)
- "6.8.2. Support for user-provided custom statistics generators" (REQUIREMENT)
648
def export_project_statistics(
649
self,
650
traceability_index: TraceabilityIndex,
651
) -> None:
652
"""
653
Export project statistics to a dedicated HTML page.654
655
@relation(SDOC-SRS-97, scope=function)656
@relation(SDOC-SRS-154, scope=function)657
"""658
659
link_renderer = LinkRenderer(
660
root_path="",
661
static_path=self.project_config.dir_for_sdoc_assets,
662
)663
664
statistics_generator = ProgressStatisticsGenerator
665
666
if (
667
custom_statistics_generator_path668
:= self.project_config.statistics_generator
669
) is not None:
670
# It is important to add the input folder to the import path.671
# Otherwise, the custom statistics generator may not be found.672
# In fact, a more reasonable path to add would be the project config673
# path, but since it is not maintained by ProjectConfig yet and674
# usually equals the input path, add the input path for675
# now.676
input_paths = self.project_config.input_paths
677
assert input_paths is not None and len(input_paths) > 0, (
678
"Expected a valid input path."679
)680
sys.path.insert(0, input_paths[0])
681
682
module_path, class_name = custom_statistics_generator_path.rsplit(
683
".", 1
684
)685
try:
686
module = importlib.import_module(module_path)
687
statistics_generator = getattr(module, class_name)
688
except ModuleNotFoundError as module_not_found_error_:
689
raise StrictDocException(
690
"Could not import a user-provided statistics generator: "691
f"{module_not_found_error_}."
692
) from module_not_found_error_
693
694
document_content = statistics_generator.export(
695
self.project_config,
696
traceability_index,
697
link_renderer,
698
html_templates=self.html_templates,
699
)700
output_html_source_coverage = os.path.join(
701
self.project_config.export_output_html_root,
702
"project_statistics.html",
703
)704
with open(output_html_source_coverage, "w", encoding="utf8") as file:
705
file.write(document_content)
706
707
@timing_decorator("Export static HTML search index")
- "6.14.1. Content search" (REQUIREMENT)
- "6.1. Static HTML search" (DESIGN)
708
def export_static_html_search_index(
709
self,
710
traceability_index: TraceabilityIndex,
711
*,
712
force_regeneration: bool = False,
713
) -> None:
714
"""
715
Export a static search index as dictionaries in .js files.716
717
@relation(SDOC-SRS-155, scope=function)718
@relation(SDOC-SRS-156, scope=function)719
"""720
721
if not force_regeneration:
722
# First check if there is nothing to do because no documents have723
# been changed or regenerated.724
725
# FIXME: This is wrong. FIX!726
must_regenerate = (
727
len(traceability_index.document_tree.document_list) == 0
728
)729
730
for document_ in traceability_index.document_tree.document_list:
731
assert document_.meta is not None
732
if traceability_index.file_dependency_manager.must_generate(
733
document_.meta.output_document_full_path
734
):735
must_regenerate = True
736
break737
738
if not must_regenerate:
739
print( # noqa: T201
740
"All documents are up-to-date. "741
"Skipping the generation of a search index."742
)743
# If no documents need to be regenerated, set the744
# search_index_timestamp to the timestamp of the first document.745
# The HTML/JS code can rely on this timestamp to decide whether746
# it has to re-read the search index from the JS file or it can747
# fetch it from the DB.748
if len(traceability_index.document_tree.document_list) > 0:
749
first_document = (
750
traceability_index.document_tree.document_list[0]
751
)752
assert first_document.meta is not None
753
traceability_index.search_index_timestamp = (
754
get_file_modification_time(
755
first_document.meta.input_doc_full_path
756
)757
)758
return759
760
if force_regeneration:
761
for document_ in traceability_index.document_tree.document_list:
762
document_.build_search_index()
763
764
global_index: Dict[str, Set[int]] = defaultdict(set)
765
global_map_nodes_by_mid: Dict[int, Dict[str, str]] = {}
766
767
document_index_list: List[Dict[str, Set[str]]] = []
768
document_map_list: List[Dict[int, Dict[str, str]]] = []
769
770
map_mid_to_numbers: Dict[str, int] = {}
771
772
with measure_performance("Build search index"):
773
for document_ in traceability_index.document_tree.document_list:
774
assert document_.meta is not None
775
document_index_list.append(
776
document_.search_index.document_index
777
)778
map_nodes_by_numbers: Dict[int, Dict[str, str]] = {}
779
for (
780
node_mid_,
781
node_dict_,
782
) in document_.search_index.map_nodes_by_mid.items():
783
if node_mid_ not in map_mid_to_numbers:
784
map_mid_to_numbers[node_mid_] = (
785
len(map_mid_to_numbers) + 1
786
)787
document_mid_number = map_mid_to_numbers[node_mid_]
788
assert isinstance(document_mid_number, int)
789
map_nodes_by_numbers[document_mid_number] = node_dict_
790
791
document_map_list.append(map_nodes_by_numbers)
792
for document_index_ in document_index_list:
793
for term_, document_mids_ in document_index_.items():
794
document_mid_numbers = set()
795
for document_mid_ in document_mids_:
796
document_mid_number = map_mid_to_numbers[document_mid_]
797
document_mid_numbers.add(document_mid_number)
798
global_index[term_].update(document_mid_numbers)
799
for map_nodes_by_mid_ in document_map_list:
800
global_map_nodes_by_mid.update(map_nodes_by_mid_)
801
802
link_renderer = LinkRenderer(
803
root_path="",
804
static_path=self.project_config.dir_for_sdoc_assets,
805
)806
for _, node_ in global_map_nodes_by_mid.items():
807
# When running on server, the MID is used as a link to the node.808
# The MID is then resolved to the correct URL by the server when809
# requested at /UID/{uid_or_mid}.810
# This ensures that all nodes can be reached with MID, including811
# the nodes that don't have a UID.812
if self.project_config.is_running_on_server:
813
node_["_LINK"] = node_["MID"]
814
815
# When running static HTML, the resolution of _LINKs happens through816
# the auto-generated static JS project_map.js that has a format of:817
# {<local anchor>: MID}818
else:
819
node = traceability_index.get_node_by_mid(MID(node_["MID"]))
820
node_["_LINK"] = link_renderer.render_local_anchor(node)
821
822
def default(obj: Any) -> Any:
823
if isinstance(obj, set):
824
return list(obj)
825
raise TypeError
826
827
with measure_performance("Serialize search index to JS"):
828
document_content = (
829
b"window.StrictDoc = window.StrictDoc || {};\n"
830
b"window.StrictDoc.search = window.StrictDoc.search || {};\n"
831
b"window.StrictDoc.search.index = "
832
+ orjson.dumps(
833
global_index,
834
option=orjson.OPT_NON_STR_KEYS,
835
default=default,
836
)837
+ b";\n\n"
838
)839
840
with measure_performance("Serialize lookup map {MID => node} to JS"):
841
document_content += (
842
b"window.StrictDoc.search.nodesByMid = "
843
+ orjson.dumps(
844
global_map_nodes_by_mid, option=orjson.OPT_NON_STR_KEYS
845
)846
+ b";\n"
847
)848
849
# Export StrictDoc's own assets.850
output_html_static_files = os.path.join(
851
self.project_config.export_output_html_root,
852
self.project_config.dir_for_sdoc_assets,
853
)854
output_html_source_coverage = os.path.join(
855
output_html_static_files,
856
"static_html_search_index.js",
857
)858
with open(output_html_source_coverage, "wb") as file:
859
file.write(document_content)
860
861
traceability_index.search_index_timestamp = get_file_modification_time(
862
output_html_source_coverage863
)864
865
def export_tree_map_screen(
866
self,
867
traceability_index: TraceabilityIndex,
868
) -> None:
869
TreeMapGenerator.export(
870
project_config=self.project_config,
871
traceability_index=traceability_index,
872
html_templates=self.html_templates,
873
)