Path:
strictdoc/export/html/html_generator.py
Lines:
795
Non-empty lines:
712
Non-empty lines covered with requirements:
712 / 712 (100.0%)
Functions:
16
Functions covered by requirements:
16 / 16 (100.0%)
1
import os
2
import shutil
3
from collections import defaultdict
4
from functools import partial
5
from pathlib import Path
6
from typing import Any, Dict, List, Optional, Set, Tuple
7
8
import orjson
9
from html2pdf4doc import PATH_TO_HTML2PDF4DOC_JS
10
11
from strictdoc.backend.sdoc.models.document import SDocDocument
12
from strictdoc.core.asset_manager import AssetDir
13
from strictdoc.core.document_meta import DocumentMeta
14
from strictdoc.core.file_system.source_tree import SourceTree
15
from strictdoc.core.project_config import ProjectConfig, ProjectFeature
16
from strictdoc.core.traceability_index import TraceabilityIndex
17
from strictdoc.export.html.document_type import DocumentType
18
from strictdoc.export.html.generators.document import DocumentHTMLGenerator
19
from strictdoc.export.html.generators.document_table import (
20
DocumentTableHTMLGenerator,
21
)22
from strictdoc.export.html.html_templates import (
23
HTMLTemplates,
24
NormalHTMLTemplates,
25
)26
from strictdoc.export.html.renderers.link_renderer import LinkRenderer
27
from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer
28
from strictdoc.features.deep_trace.generator import (
29
DocumentDeepTraceHTMLGenerator,
30
)31
from strictdoc.features.html2pdf.generator import (
32
DocumentHTML2PDFGenerator,
33
)34
from strictdoc.features.project_index.generator import (
35
DocumentTreeHTMLGenerator,
36
)37
from strictdoc.features.project_index.project_map_generator import (
38
ProjectMapGenerator,
39
)40
from strictdoc.features.source_coverage.generator import (
41
SourceFileCoverageHTMLGenerator,
42
)43
from strictdoc.features.source_file_view.generator import (
44
SourceFileViewHTMLGenerator,
45
)46
from strictdoc.features.trace.generator import (
47
DocumentTraceHTMLGenerator,
48
)49
from strictdoc.features.traceability_matrix.generator import (
50
TraceabilityMatrixHTMLGenerator,
51
)52
from strictdoc.features.tree_map.generator import TreeMapGenerator
53
from strictdoc.helpers.cast import assert_cast
54
from strictdoc.helpers.file_modification_time import get_file_modification_time
55
from strictdoc.helpers.file_system import sync_dir
56
from strictdoc.helpers.git_client import GitClient
57
from strictdoc.helpers.mid import MID
58
from strictdoc.helpers.parallelizer import Parallelizer
59
from strictdoc.helpers.paths import SDocRelativePath, path_to_posix_path
60
from strictdoc.helpers.timing import measure_performance, timing_decorator
61
62
63
def render_favicon_svg(
64
project_config: ProjectConfig,
65
html_templates: HTMLTemplates, # noqa: ARG001
66
) -> str:
67
# Deliberately not using html_templates.jinja_environment(): for large68
# projects it is a CompiledHTMLTemplates instance that lazily caches a69
# ModuleLoader-backed Environment (holding unpicklable compiled70
# _TemplateModule objects) on first call. HTMLGenerator instances are71
# captured by the closure passed to the document-export parallelizer,72
# so populating that cache here, in the main process, before parallel73
# export starts, makes the whole HTMLGenerator (and its html_templates)74
# unpicklable for the worker pool. A standalone, uncached environment75
# sidesteps that entirely; favicon.svg.jinja is small enough that76
# skipping template compilation has no measurable cost.77
variant = project_config.get_favicon_variant()
78
return (
79
NormalHTMLTemplates()
80
.jinja_environment()
81
.get_template("_shared/favicon.svg.jinja")
82
.render(variant=variant)
83
)84
85
86
class HTMLGenerator:
87
def __init__(
88
self, project_config: ProjectConfig, html_templates: HTMLTemplates
89
):90
self.project_config: ProjectConfig = project_config
91
self.html_templates = html_templates
92
self.git_client: GitClient = GitClient()
93
94
def export_complete_tree(
95
self,
96
*,
97
traceability_index: TraceabilityIndex,
98
parallelizer: Parallelizer,
99
) -> None:
100
Path(self.project_config.export_output_html_root).mkdir(
101
parents=True, exist_ok=True
102
)103
104
# Export assets.105
HTMLGenerator.export_assets(
106
traceability_index=traceability_index,
107
project_config=self.project_config,
108
html_templates=self.html_templates,
109
export_output_html_root=self.project_config.export_output_html_root,
110
)111
112
# Export static search index.113
self.export_static_html_search_index(
114
traceability_index=traceability_index
115
)116
117
# Export all documents in parallel.118
export_binding = partial(
119
self.export_single_document_with_performance,
120
traceability_index=traceability_index,
121
)122
123
# By default, do not export included documents. Only, if the option to124
# include is provided.125
documents_to_export: List[SDocDocument] = []
126
127
if self.project_config.export_included_documents:
128
documents_to_export[:] = (
129
traceability_index.document_tree.document_list
130
)131
else:
132
for document_ in traceability_index.document_tree.document_list:
133
if document_.document_is_included():
134
continue135
136
document_meta = assert_cast(document_.meta, DocumentMeta)
137
138
input_doc_full_path = document_meta.input_doc_full_path
139
output_doc_full_path = document_meta.output_document_full_path
140
141
if os.path.isfile(output_doc_full_path) and (
142
get_file_modification_time(input_doc_full_path)
143
< get_file_modification_time(output_doc_full_path)
144
and not traceability_index.file_dependency_manager.must_generate(
145
document_meta.output_document_full_path
146
)147
):148
with measure_performance(f"Skip: {document_.title}"):
149
continue150
151
documents_to_export.append(document_)
152
153
if len(documents_to_export) > 0:
154
if len(traceability_index.document_tree.document_list) <= 25:
155
parallelizer.run_parallel(documents_to_export, export_binding)
156
else:
157
print( # noqa: T201
158
"NOTE: Running document export without parallelization "159
"because the document tree contains more than 25 documents."160
)161
for document_ in documents_to_export:
162
export_binding(document_)
163
164
# Export document tree.165
# FIXME: It is important that this export is **after** the parallelized166
# export of single documents. It turns out that Jinja does not play167
# well with the multiprocessing's processed-based parallelization.168
# _pickle.PicklingError: Can't pickle <function sync_do_first at 0x1077bdf80>: it's not the same object as jinja2.filters.sync_do_first.169
self.export_project_tree_screen(traceability_index=traceability_index)
170
171
# Export JavaScript map of the document tree (project map)172
self.export_project_map(traceability_index=traceability_index)
173
174
if self.project_config.is_activated_tree_map():
175
self.export_tree_map_screen(traceability_index)
176
177
# Project statistics is exported by the ExportAction class via the178
# Feature abstraction (see the ProjectStatisticsFeature class), not179
# here.180
181
# Export requirements coverage.182
if self.project_config.is_feature_activated(
183
ProjectFeature.TRACEABILITY_MATRIX_SCREEN
184
):185
self.export_requirements_coverage_screen(
186
traceability_index=traceability_index,
187
)188
189
# Export source coverage.190
if self.project_config.is_feature_activated(
191
ProjectFeature.REQUIREMENT_TO_SOURCE_TRACEABILITY
192
):193
self.export_source_files_screens(
194
traceability_index=traceability_index,
195
)196
self.export_source_coverage_screen(
197
traceability_index=traceability_index,
198
)199
200
print( # noqa: T201
201
"Export completed. Documentation tree can be found at:\n"
202
f"{self.project_config.export_output_html_root}"
203
)204
205
@staticmethod206
def export_assets(
207
*,
208
traceability_index: Optional[TraceabilityIndex],
209
project_config: ProjectConfig,
210
html_templates: HTMLTemplates,
211
export_output_html_root: str,
212
flat_assets: bool = False,
213
) -> None:
214
"""
215
Copy all assets to output dir during HTML/PDF generation.216
217
:param bool flat_assets: This parameter is always set to False except when218
exporting a "bundle document" with HTML2PDF.219
The bundle document contains all documents of220
the documentation tree. In this case, all assets221
are simply copied to the top level _assets folder,222
independently on how nested the contained documents are.223
"""224
225
# Export StrictDoc's own assets.226
output_html_static_files = os.path.join(
227
export_output_html_root,
228
project_config.dir_for_sdoc_assets,
229
)230
for static_files_path in project_config.get_static_files_paths():
231
sync_dir(
232
static_files_path,
233
output_html_static_files,
234
message="Copying StrictDoc's assets",
235
)236
237
# Write the favicon: a project's own custom file (only for the238
# "default" variant, see ProjectConfig.get_custom_favicon_path()),239
# or else render it from the Jinja template so it can encode which240
# kind of StrictDoc instance (dev/test/docs export) rendered it.241
favicon_output_path = os.path.join(
242
output_html_static_files, project_config.get_favicon_filename()
243
)244
custom_favicon_path = project_config.get_custom_favicon_path()
245
if custom_favicon_path is not None:
246
shutil.copyfile(custom_favicon_path, favicon_output_path)
247
else:
248
favicon_svg = render_favicon_svg(project_config, html_templates)
249
with open(favicon_output_path, "w", encoding="utf8") as output_file:
250
output_file.write(favicon_svg)
251
252
# Copy the project's custom CSS file, if configured. base.jinja.html253
# links it after StrictDoc's own stylesheets so it can override them.254
custom_css_path = project_config.custom_css_path
255
if custom_css_path is not None:
256
shutil.copyfile(
257
custom_css_path,
258
os.path.join(
259
output_html_static_files,
260
project_config.get_custom_css_filename(),
261
),262
)263
264
# Export HTML2PDF.265
if project_config.is_feature_activated(ProjectFeature.HTML2PDF):
266
sync_dir(
267
os.path.dirname(PATH_TO_HTML2PDF4DOC_JS),
268
output_html_static_files,
269
message="Copying HTML2PDF.js",
270
)271
272
# Export custom html2pdf template.273
if project_config.html2pdf_template is not None:
274
output_custom_html2pdf_template = os.path.join(
275
export_output_html_root,
276
project_config.dir_for_sdoc_assets,
277
"html2pdf_template",
278
)279
sync_dir(
280
os.path.abspath(
281
os.path.dirname(project_config.html2pdf_template)
282
),283
output_custom_html2pdf_template,
284
message="Copying Custom HTML2PDF template assets",
285
)286
287
# Export project's assets.288
289
if traceability_index is not None:
290
redundant_assets: Dict[str, List[SDocRelativePath]] = {}
291
for document_ in traceability_index.document_tree.document_list:
292
assert document_.meta is not None
293
for (
294
included_document_295
) in document_.iterate_included_documents_depth_first():
296
assert included_document_.meta is not None
297
298
redundant_assets.setdefault(
299
document_.meta.input_doc_assets_dir_rel_path.relative_path_posix,
300
[],301
)302
redundant_assets[
303
document_.meta.input_doc_assets_dir_rel_path.relative_path_posix
304
].append(
305
included_document_.meta.input_doc_assets_dir_rel_path
306
)307
308
assert traceability_index.asset_manager is not None
309
310
asset_dir_: AssetDir
311
for asset_dir_ in traceability_index.asset_manager.iterate():
312
source_path = asset_dir_.full_path
313
output_relative_path = asset_dir_.relative_path
314
315
destination_path = os.path.join(
316
export_output_html_root,
317
output_relative_path.relative_path
318
if not flat_assets
319
else "_assets",
320
)321
322
sync_dir(
323
source_path,
324
destination_path,
325
message=f'Copying project assets "{output_relative_path.relative_path}"',
326
)327
redundant_asset_paths = redundant_assets.get(
328
output_relative_path.relative_path_posix
329
)330
if redundant_asset_paths is not None:
331
for redundant_asset_ in redundant_asset_paths:
332
destination_path = os.path.join(
333
export_output_html_root,
334
redundant_asset_.relative_path
335
if not flat_assets
336
else "_assets",
337
)338
sync_dir(
339
source_path,
340
destination_path,
341
message=f'Copying project assets "{output_relative_path.relative_path}"',
342
)343
344
def export_single_document_with_performance(
345
self,
346
document: SDocDocument,
347
traceability_index: TraceabilityIndex,
348
specific_documents: Optional[Tuple[DocumentType, ...]] = None,
349
) -> None:
350
if specific_documents is None:
351
specific_documents = DocumentType.all()
352
353
with measure_performance(f"Published: {document.title}"):
354
self.export_single_document(
355
document,
356
traceability_index,
357
specific_documents=specific_documents,
358
)359
360
def export_single_document(
361
self,
362
document: SDocDocument,
363
traceability_index: TraceabilityIndex,
364
specific_documents: Optional[Tuple[DocumentType, ...]] = None,
365
) -> SDocDocument:
366
if document.config.layout == "Website":
367
specific_documents = (DocumentType.DOCUMENT,)
368
elif specific_documents is None:
369
specific_documents = DocumentType.all()
370
371
assert document.meta is not None
372
373
document_meta: DocumentMeta = document.meta
374
375
document_output_folder = document_meta.output_document_dir_full_path
376
Path(document_output_folder).mkdir(parents=True, exist_ok=True)
377
378
root_path = document.meta.get_root_path_prefix()
379
link_renderer = LinkRenderer(
380
root_path=root_path,
381
static_path=self.project_config.dir_for_sdoc_assets,
382
)383
markup_renderer = MarkupRenderer.create(
384
markup=document.config.markup,
385
traceability_index=traceability_index,
386
link_renderer=link_renderer,
387
html_templates=self.html_templates,
388
config=self.project_config,
389
context_document=document,
390
)391
392
if DocumentType.DOCUMENT in specific_documents:
393
# Single Document pages.394
document_content = DocumentHTMLGenerator.export(
395
project_config=self.project_config,
396
document=document,
397
traceability_index=traceability_index,
398
markup_renderer=markup_renderer,
399
link_renderer=link_renderer,
400
git_client=self.git_client,
401
html_templates=self.html_templates,
402
)403
document_out_file = document_meta.get_html_doc_path()
404
with open(document_out_file, "w", encoding="utf8") as file:
405
file.write(document_content)
406
407
# Single Document Table pages.408
if (
409
self.project_config.is_feature_activated(
410
ProjectFeature.TABLE_SCREEN
411
)412
and DocumentType.TABLE in specific_documents
413
):414
document_content = DocumentTableHTMLGenerator.export(
415
project_config=self.project_config,
416
document=document,
417
traceability_index=traceability_index,
418
markup_renderer=markup_renderer,
419
link_renderer=link_renderer,
420
git_client=self.git_client,
421
html_templates=self.html_templates,
422
)423
document_out_file = document_meta.get_html_table_path()
424
with open(document_out_file, "w", encoding="utf8") as file:
425
file.write(document_content)
426
427
# Single Document Traceability pages.428
if (
429
self.project_config.is_feature_activated(
430
ProjectFeature.TRACEABILITY_SCREEN
431
)432
and DocumentType.TRACE in specific_documents
433
):434
document_content = DocumentTraceHTMLGenerator.export(
435
project_config=self.project_config,
436
document=document,
437
traceability_index=traceability_index,
438
markup_renderer=markup_renderer,
439
link_renderer=link_renderer,
440
git_client=self.git_client,
441
html_templates=self.html_templates,
442
)443
document_out_file = document_meta.get_html_traceability_path()
444
with open(document_out_file, "w", encoding="utf8") as file:
445
file.write(document_content)
446
447
# Single Document Deep Traceability pages.448
if (
449
self.project_config.is_feature_activated(
450
ProjectFeature.DEEP_TRACEABILITY_SCREEN
451
)452
and DocumentType.DEEPTRACE in specific_documents
453
):454
document_content = DocumentDeepTraceHTMLGenerator.export_deep(
455
project_config=self.project_config,
456
document=document,
457
traceability_index=traceability_index,
458
markup_renderer=markup_renderer,
459
link_renderer=link_renderer,
460
git_client=self.git_client,
461
html_templates=self.html_templates,
462
)463
document_out_file = document_meta.get_html_deep_traceability_path()
464
with open(document_out_file, "w", encoding="utf8") as file:
465
file.write(document_content)
466
467
# Single Document PDF pages.468
if (
469
self.project_config.is_feature_activated(ProjectFeature.HTML2PDF)
470
and DocumentType.PDF in specific_documents
471
):472
document_content = DocumentHTML2PDFGenerator.export(
473
project_config=self.project_config,
474
document=document,
475
traceability_index=traceability_index,
476
markup_renderer=markup_renderer,
477
link_renderer=link_renderer,
478
git_client=self.git_client,
479
html_templates=self.html_templates,
480
)481
document_out_file = document_meta.get_html_pdf_path()
482
with open(document_out_file, "w", encoding="utf8") as file:
483
file.write(document_content)
484
485
return document
486
487
def export_project_tree_screen(
488
self,
489
*,
490
traceability_index: TraceabilityIndex,
491
) -> None:
492
Path(self.project_config.export_output_html_root).mkdir(
493
parents=True, exist_ok=True
494
)495
output_file = os.path.join(
496
self.project_config.export_output_html_root, "index.html"
497
)498
writer = DocumentTreeHTMLGenerator()
499
output = writer.export(
500
self.project_config,
501
traceability_index=traceability_index,
502
html_templates=self.html_templates,
503
)504
with open(output_file, "w", encoding="utf8") as file:
505
file.write(output)
506
507
def export_project_map(
508
self,
509
*,
510
traceability_index: TraceabilityIndex,
511
) -> None:
512
assets_dir = os.path.join(
513
self.project_config.export_output_html_root,
514
self.project_config.dir_for_sdoc_assets,
515
)516
output_file = os.path.join(assets_dir, "project_map.js")
517
writer = ProjectMapGenerator()
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_requirements_coverage_screen(
527
self,
528
*,
529
traceability_index: TraceabilityIndex,
530
) -> None:
531
requirements_coverage_content = TraceabilityMatrixHTMLGenerator.export(
532
project_config=self.project_config,
533
traceability_index=traceability_index,
534
html_templates=self.html_templates,
535
)536
output_html_requirements_coverage = os.path.join(
537
self.project_config.export_output_html_root,
538
"traceability_matrix.html",
539
)540
with open(
541
output_html_requirements_coverage, "w", encoding="utf8"
542
) as file:
543
file.write(requirements_coverage_content)
544
545
@timing_decorator("Export source file pages")
546
def export_source_files_screens(
547
self,
548
*,
549
traceability_index: TraceabilityIndex,
550
) -> None:
551
assert isinstance(
552
traceability_index.document_tree.source_tree, SourceTree
553
), traceability_index.document_tree.source_tree
554
print("Generating source files:") # noqa: T201
555
for (
556
source_file557
) in traceability_index.document_tree.source_tree.source_files:
558
if not source_file.is_referenced:
559
continue560
561
SourceFileViewHTMLGenerator.export_to_file(
562
project_config=self.project_config,
563
source_file=source_file,
564
traceability_index=traceability_index,
565
html_templates=self.html_templates,
566
)567
568
def export_source_coverage_screen(
569
self,
570
*,
571
traceability_index: TraceabilityIndex,
572
) -> None:
573
assert isinstance(
574
traceability_index.document_tree.source_tree, SourceTree
575
), traceability_index.document_tree.source_tree
576
577
source_coverage_content = SourceFileCoverageHTMLGenerator.export(
578
project_config=self.project_config,
579
traceability_index=traceability_index,
580
html_templates=self.html_templates,
581
)582
output_html_source_coverage = os.path.join(
583
self.project_config.export_output_html_root, "source_coverage.html"
584
)585
with open(output_html_source_coverage, "w", encoding="utf8") as file:
586
file.write(source_coverage_content)
587
588
def export_single_source_file_screen(
589
self,
590
*,
591
traceability_index: TraceabilityIndex,
592
path_to_source_file: str,
593
) -> None:
594
assert isinstance(
595
traceability_index.document_tree.source_tree, SourceTree
596
), traceability_index.document_tree.source_tree
597
598
# FIXME: path_to_source_file must not enter this function with forward slashes.599
# Test and fix this on Windows.600
# https://github.com/strictdoc-project/strictdoc/issues/2068601
relative_path_to_source_file = path_to_posix_path(path_to_source_file)
602
relative_path_to_source_file = (
603
relative_path_to_source_file.removeprefix("_source_files/")
604
)605
relative_path_to_source_file = (
606
relative_path_to_source_file.removesuffix(".html")
607
)608
609
for (
610
source_file611
) in traceability_index.document_tree.source_tree.source_files:
612
if not source_file.is_referenced:
613
continue614
615
if (
616
relative_path_to_source_file617
== source_file.in_doctree_source_file_rel_path_posix
618
):619
SourceFileViewHTMLGenerator.export_to_file(
620
project_config=self.project_config,
621
source_file=source_file,
622
traceability_index=traceability_index,
623
html_templates=self.html_templates,
624
)625
return626
627
raise FileNotFoundError
628
629
@timing_decorator("Export static HTML search index")
- "6.14.1. Content search" (REQUIREMENT)
- "6.1. Static HTML search" (DESIGN)
630
def export_static_html_search_index(
631
self,
632
traceability_index: TraceabilityIndex,
633
*,
634
force_regeneration: bool = False,
635
) -> None:
636
"""
637
Export a static search index as dictionaries in .js files.638
639
@relation(SDOC-SRS-155, scope=function)640
@relation(SDOC-SRS-156, scope=function)641
"""642
643
if not force_regeneration:
644
# First check if there is nothing to do because no documents have645
# been changed or regenerated.646
647
# FIXME: This is wrong. FIX!648
must_regenerate = (
649
len(traceability_index.document_tree.document_list) == 0
650
)651
652
for document_ in traceability_index.document_tree.document_list:
653
assert document_.meta is not None
654
if traceability_index.file_dependency_manager.must_generate(
655
document_.meta.output_document_full_path
656
):657
must_regenerate = True
658
break659
660
if not must_regenerate:
661
print( # noqa: T201
662
"All documents are up-to-date. "663
"Skipping the generation of a search index."664
)665
# If no documents need to be regenerated, set the666
# search_index_timestamp to the timestamp of the first document.667
# The HTML/JS code can rely on this timestamp to decide whether668
# it has to re-read the search index from the JS file or it can669
# fetch it from the DB.670
if len(traceability_index.document_tree.document_list) > 0:
671
first_document = (
672
traceability_index.document_tree.document_list[0]
673
)674
assert first_document.meta is not None
675
traceability_index.search_index_timestamp = (
676
get_file_modification_time(
677
first_document.meta.input_doc_full_path
678
)679
)680
return681
682
if force_regeneration:
683
for document_ in traceability_index.document_tree.document_list:
684
document_.build_search_index()
685
686
global_index: Dict[str, Set[int]] = defaultdict(set)
687
global_map_nodes_by_mid: Dict[int, Dict[str, str]] = {}
688
689
document_index_list: List[Dict[str, Set[str]]] = []
690
document_map_list: List[Dict[int, Dict[str, str]]] = []
691
692
map_mid_to_numbers: Dict[str, int] = {}
693
694
with measure_performance("Build search index"):
695
for document_ in traceability_index.document_tree.document_list:
696
assert document_.meta is not None
697
document_index_list.append(
698
document_.search_index.document_index
699
)700
map_nodes_by_numbers: Dict[int, Dict[str, str]] = {}
701
for (
702
node_mid_,
703
node_dict_,
704
) in document_.search_index.map_nodes_by_mid.items():
705
if node_mid_ not in map_mid_to_numbers:
706
map_mid_to_numbers[node_mid_] = (
707
len(map_mid_to_numbers) + 1
708
)709
document_mid_number = map_mid_to_numbers[node_mid_]
710
assert isinstance(document_mid_number, int)
711
map_nodes_by_numbers[document_mid_number] = node_dict_
712
713
document_map_list.append(map_nodes_by_numbers)
714
for document_index_ in document_index_list:
715
for term_, document_mids_ in document_index_.items():
716
document_mid_numbers = set()
717
for document_mid_ in document_mids_:
718
document_mid_number = map_mid_to_numbers[document_mid_]
719
document_mid_numbers.add(document_mid_number)
720
global_index[term_].update(document_mid_numbers)
721
for map_nodes_by_mid_ in document_map_list:
722
global_map_nodes_by_mid.update(map_nodes_by_mid_)
723
724
link_renderer = LinkRenderer(
725
root_path="",
726
static_path=self.project_config.dir_for_sdoc_assets,
727
)728
for _, node_ in global_map_nodes_by_mid.items():
729
# When running on server, the MID is used as a link to the node.730
# The MID is then resolved to the correct URL by the server when731
# requested at /UID/{uid_or_mid}.732
# This ensures that all nodes can be reached with MID, including733
# the nodes that don't have a UID.734
if self.project_config.is_running_on_server:
735
node_["_LINK"] = node_["MID"]
736
737
# When running static HTML, the resolution of _LINKs happens through738
# the auto-generated static JS project_map.js that has a format of:739
# {<local anchor>: MID}740
else:
741
node = traceability_index.get_node_by_mid(MID(node_["MID"]))
742
node_["_LINK"] = link_renderer.render_local_anchor(node)
743
744
def default(obj: Any) -> Any:
745
if isinstance(obj, set):
746
return list(obj)
747
raise TypeError
748
749
with measure_performance("Serialize search index to JS"):
750
document_content = (
751
b"window.StrictDoc = window.StrictDoc || {};\n"
752
b"window.StrictDoc.search = window.StrictDoc.search || {};\n"
753
b"window.StrictDoc.search.index = "
754
+ orjson.dumps(
755
global_index,
756
option=orjson.OPT_NON_STR_KEYS,
757
default=default,
758
)759
+ b";\n\n"
760
)761
762
with measure_performance("Serialize lookup map {MID => node} to JS"):
763
document_content += (
764
b"window.StrictDoc.search.nodesByMid = "
765
+ orjson.dumps(
766
global_map_nodes_by_mid, option=orjson.OPT_NON_STR_KEYS
767
)768
+ b";\n"
769
)770
771
# Export StrictDoc's own assets.772
output_html_static_files = os.path.join(
773
self.project_config.export_output_html_root,
774
self.project_config.dir_for_sdoc_assets,
775
)776
output_html_source_coverage = os.path.join(
777
output_html_static_files,
778
"static_html_search_index.js",
779
)780
with open(output_html_source_coverage, "wb") as file:
781
file.write(document_content)
782
783
traceability_index.search_index_timestamp = get_file_modification_time(
784
output_html_source_coverage785
)786
787
def export_tree_map_screen(
788
self,
789
traceability_index: TraceabilityIndex,
790
) -> None:
791
TreeMapGenerator.export(
792
project_config=self.project_config,
793
traceability_index=traceability_index,
794
html_templates=self.html_templates,
795
)