Path:
strictdoc/features/source_file_view/generator.py
Lines:
306
Non-empty lines:
281
Non-empty lines covered with requirements:
281 / 281 (100.0%)
Functions:
5
Functions covered by requirements:
5 / 5 (100.0%)
1
"""2
@relation(SDOC-SRS-36, scope=file)3
"""4
5
# mypy: disable-error-code="operator"6
from pathlib import Path
7
from typing import List, Tuple, Union
8
9
from markupsafe import Markup
10
from pygments import highlight
11
from pygments.formatters.html import HtmlFormatter
12
from pygments.lexers import get_lexer_for_filename
13
from pygments.lexers.c_cpp import CLexer, CppLexer
14
from pygments.lexers.configs import TOMLLexer
15
from pygments.lexers.data import YamlLexer
16
from pygments.lexers.javascript import JavascriptLexer
17
from pygments.lexers.markup import RstLexer, TexLexer
18
from pygments.lexers.python import PythonLexer
19
from pygments.lexers.special import TextLexer
20
from pygments.lexers.templates import HtmlDjangoLexer
21
from pygments.util import ClassNotFound
22
23
from strictdoc.backend.sdoc.constants import SDocMarkup
24
from strictdoc.backend.sdoc_source_code.models.language_item_marker import (
25
ForwardLanguageItemMarker,
26
LanguageItemMarker,
27
)28
from strictdoc.backend.sdoc_source_code.models.line_marker import LineMarker
29
from strictdoc.backend.sdoc_source_code.models.range_marker import (
30
ForwardRangeMarker,
31
RangeMarker,
32
)33
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
34
SourceFileTraceabilityInfo,
35
)36
from strictdoc.core.file_system.source_tree import SourceFile
37
from strictdoc.core.project_config import ProjectConfig
38
from strictdoc.core.traceability_index import TraceabilityIndex
39
from strictdoc.export.html.html_templates import HTMLTemplates
40
from strictdoc.export.html.renderers.link_renderer import LinkRenderer
41
from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer
42
from strictdoc.features.source_file_view.view_object import (
43
SourceFileViewObject,
44
SourceLineEntry,
45
SourceMarkerTuple,
46
)47
from strictdoc.helpers.cast import assert_cast
48
from strictdoc.helpers.file_system import file_open_read_utf8
49
from strictdoc.helpers.timing import measure_performance
50
51
52
class SourceFileViewHTMLGenerator:
53
@staticmethod54
def export_to_file(
55
*,
56
project_config: ProjectConfig,
57
source_file: SourceFile,
58
traceability_index: TraceabilityIndex,
59
html_templates: HTMLTemplates,
60
) -> None:
61
if not traceability_index.file_dependency_manager.must_generate(
62
source_file.output_file_full_path
63
):64
with measure_performance(
65
f"Skip: {source_file.in_doctree_source_file_rel_path}"
66
):67
return68
69
with measure_performance(
70
f"File: {source_file.in_doctree_source_file_rel_path}"
71
):72
document_content = SourceFileViewHTMLGenerator.export(
73
project_config=project_config,
74
source_file=source_file,
75
traceability_index=traceability_index,
76
html_templates=html_templates,
77
)78
Path(source_file.output_dir_full_path).mkdir(
79
parents=True, exist_ok=True
80
)81
with open(
82
source_file.output_file_full_path, "w", encoding="utf-8"
83
) as file:
84
file.write(document_content)
85
86
@staticmethod87
def export(
88
*,
89
project_config: ProjectConfig,
90
source_file: SourceFile,
91
traceability_index: TraceabilityIndex,
92
html_templates: HTMLTemplates,
93
) -> Markup:
94
with file_open_read_utf8(source_file.full_path) as opened_file:
95
source_file_lines = opened_file.readlines()
96
97
pygmented_source_file_lines: List[SourceLineEntry] = []
98
pygments_styles: Markup = Markup("")
99
100
trace_info: SourceFileTraceabilityInfo = (
101
traceability_index.get_coverage_info(
102
source_file.in_doctree_source_file_rel_path_posix
103
)104
)105
106
if len(source_file_lines) > 0:
107
(108
pygmented_source_file_lines,
109
pygments_styles,
110
) = SourceFileViewHTMLGenerator.get_pygmented_source_lines(
111
source_file, source_file_lines, trace_info
112
)113
link_renderer = LinkRenderer(
114
root_path=source_file.path_depth_prefix,
115
static_path=project_config.dir_for_sdoc_assets,
116
)117
markup_renderer = MarkupRenderer.create(
118
SDocMarkup.RST,
119
traceability_index,
120
link_renderer,
121
html_templates,
122
project_config,
123
None,
124
)125
text_renderer = MarkupRenderer.create(
126
SDocMarkup.TEXT,
127
traceability_index,
128
link_renderer,
129
html_templates,
130
project_config,
131
None,
132
)133
view_object = SourceFileViewObject(
134
traceability_index=traceability_index,
135
trace_info=trace_info,
136
project_config=project_config,
137
link_renderer=link_renderer,
138
markup_renderer=markup_renderer,
139
text_renderer=text_renderer,
140
source_file=source_file,
141
pygments_styles=pygments_styles,
142
pygmented_source_file_lines=pygmented_source_file_lines,
143
jinja_environment=html_templates.jinja_environment(),
144
)145
return view_object.render_screen()
146
147
@staticmethod148
def get_pygmented_source_lines(
149
source_file: SourceFile,
150
source_file_lines: List[str],
151
coverage_info: SourceFileTraceabilityInfo,
152
) -> Tuple[
153
List[SourceLineEntry],
154
Markup,
155
]:156
assert isinstance(source_file, SourceFile)
157
assert isinstance(source_file_lines, list)
158
assert isinstance(coverage_info, SourceFileTraceabilityInfo)
159
160
if source_file.is_python_file():
161
lexer = PythonLexer()
162
elif source_file.is_c_file():
163
lexer = CLexer()
164
elif source_file.is_cpp_file():
165
lexer = CppLexer()
166
elif source_file.is_tex_file():
167
lexer = TexLexer()
168
elif source_file.is_toml_file():
169
lexer = TOMLLexer()
170
elif source_file.is_jinja_file():
171
lexer = HtmlDjangoLexer()
172
elif source_file.is_javascript_file():
173
lexer = JavascriptLexer()
174
elif source_file.is_yaml_file():
175
lexer = YamlLexer()
176
elif source_file.is_rst_file():
177
lexer = RstLexer()
178
elif source_file.is_st_file():
179
# Structured Text syntax doesn't seem to be supported by Pygments.180
lexer = TextLexer()
181
else:
182
try:
183
lexer = get_lexer_for_filename(source_file.file_name)
184
except ClassNotFound:
185
lexer = TextLexer()
186
187
# HACK:188
# Otherwise, Pygments will skip the first line as if it does not exist.189
# This behavior surprisingly has an effect on the first line if its empty.190
hack_first_line: bool = False
191
if source_file_lines[0] == "\n":
192
source_file_lines[0] = " \n"
193
hack_first_line = True
194
195
# HACK:196
# Pygments does not process lines if they are empty and are at the end197
# of a file. Adding a marker to the end so that Pygments do not cut the198
# corners.199
source_file_content = "".join(source_file_lines)
200
source_file_content_with_marker = source_file_content + "\n###"
201
202
html_formatter = HtmlFormatter()
203
pygmented_source_file_content = highlight(
204
source_file_content_with_marker, lexer, html_formatter
205
)206
207
# HACK: split content into lines by cutting off the header and footer208
# parts generated by Pygments:209
# <div class="highlight"><pre> and </pre></div>210
# TODO: Implement proper splitting.211
start_pattern = '<div class="highlight"><pre>'
212
end_pattern = "</pre></div>\n"
213
assert pygmented_source_file_content.startswith(start_pattern)
214
assert pygmented_source_file_content.endswith(end_pattern), (
215
f"{pygmented_source_file_content}"
216
)217
218
slice_start = len(start_pattern)
219
slice_end = len(pygmented_source_file_content) - len(end_pattern)
220
pygmented_source_file_content = pygmented_source_file_content[
221
slice_start:slice_end
222
]223
pygmented_source_file_lines: List[Union[str, SourceMarkerTuple]] = list(
224
pygmented_source_file_content.split("\n")
225
)226
if hack_first_line:
227
pygmented_source_file_lines[0] = "<span></span>"
228
229
if pygmented_source_file_lines[-1] == "":
230
pygmented_source_file_lines.pop()
231
assert "###" in pygmented_source_file_lines[-1], (
232
"Expected marker to be in place."233
)234
# Pop ###, pop "\n"235
pygmented_source_file_lines.pop()
236
if pygmented_source_file_lines[-1] == "":
237
pygmented_source_file_lines.pop()
238
239
assert len(pygmented_source_file_lines) == len(source_file_lines), (
240
f"Something went wrong when running Pygments against "
241
f"the source file: "
242
f"{len(pygmented_source_file_lines)} == {len(source_file_lines)}, "
243
f"{pygmented_source_file_lines} == {source_file_lines}."
244
)245
246
for marker in coverage_info.markers:
247
marker_line = (
248
marker.ng_range_line_begin
249
if marker.is_begin()
250
else marker.ng_range_line_end
251
)252
assert isinstance(marker_line, int)
253
source_marker_tuple: SourceMarkerTuple
254
if isinstance(
255
pygmented_source_file_lines[marker_line - 1], SourceMarkerTuple
256
):257
source_marker_tuple = assert_cast(
258
pygmented_source_file_lines[marker_line - 1],
259
SourceMarkerTuple,
260
)261
else:
262
pygmented_source_file_line = assert_cast(
263
pygmented_source_file_lines[marker_line - 1], str
264
)265
assert marker.ng_range_line_begin is not None
266
assert marker.ng_range_line_end is not None
267
source_marker_tuple = SourceMarkerTuple(
268
ng_range_line_begin=marker.ng_range_line_begin,
269
ng_range_line_end=marker.ng_range_line_end,
270
source_line=Markup(pygmented_source_file_line),
271
markers=[],
272
)273
pygmented_source_file_lines[marker_line - 1] = (
274
source_marker_tuple275
)276
277
if isinstance(
278
marker,
279
(280
ForwardRangeMarker,
281
ForwardLanguageItemMarker,
282
LanguageItemMarker,
283
RangeMarker,
284
LineMarker,
285
),286
):287
source_marker_tuple.markers.append(marker)
288
continue289
290
raise NotImplementedError(marker) # pragma: no cover
291
292
pygments_styles = (
293
f"/* Lexer: {lexer.name} */\n"
294
+ html_formatter.get_style_defs(".highlight")
295
)296
297
return [
298
SourceFileViewHTMLGenerator.mark_safe(line)
299
for line in pygmented_source_file_lines
300
], Markup(pygments_styles)
301
302
@staticmethod303
def mark_safe(
304
line: Union[str, SourceMarkerTuple],
305
) -> Union[Markup, SourceMarkerTuple]:
306
return line if isinstance(line, SourceMarkerTuple) else Markup(line)