diff --git a/strictdoc/export/html/generators/source_file_view_generator.py b/strictdoc/export/html/generators/source_file_view_generator.py index de36ccde1..ac485ce19 100644 --- a/strictdoc/export/html/generators/source_file_view_generator.py +++ b/strictdoc/export/html/generators/source_file_view_generator.py @@ -1,5 +1,5 @@ # mypy: disable-error-code="no-untyped-call,no-untyped-def,operator" -from typing import List, Tuple +from typing import List, Tuple, Union from markupsafe import Markup, escape from pygments import highlight @@ -28,10 +28,13 @@ from strictdoc.core.traceability_index import TraceabilityIndex from strictdoc.export.html.generators.view_objects.source_file_view_object import ( SourceFileViewObject, + SourceLineEntry, + SourceMarkerTuple, ) from strictdoc.export.html.html_templates import HTMLTemplates from strictdoc.export.html.renderers.link_renderer import LinkRenderer from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer +from strictdoc.helpers.cast import assert_cast class SourceFileViewHTMLGenerator: @@ -46,7 +49,7 @@ def export( with open(source_file.full_path, encoding="utf-8") as opened_file: source_file_lines = opened_file.readlines() - pygmented_source_file_lines: List[Markup] = [] + pygmented_source_file_lines: List[SourceLineEntry] = [] pygments_styles: Markup = Markup("") if len(source_file_lines) > 0: @@ -89,7 +92,10 @@ def get_pygmented_source_lines( source_file: SourceFile, source_file_lines: List[str], coverage_info: SourceFileTraceabilityInfo, - ) -> Tuple[List[Markup], Markup]: + ) -> Tuple[ + List[SourceLineEntry], + Markup, + ]: assert isinstance(source_file, SourceFile) assert isinstance(source_file_lines, list) assert isinstance(coverage_info, SourceFileTraceabilityInfo) @@ -154,7 +160,9 @@ def get_pygmented_source_lines( pygmented_source_file_content = pygmented_source_file_content[ slice_start:slice_end ] - pygmented_source_file_lines = pygmented_source_file_content.split("\n") + pygmented_source_file_lines: List[Union[str, SourceMarkerTuple]] = list( + pygmented_source_file_content.split("\n") + ) if hack_first_line: pygmented_source_file_lines[0] = "" @@ -177,12 +185,14 @@ def get_pygmented_source_lines( for pragma in coverage_info.pragmas: pragma_line = pragma.ng_source_line_begin + assert isinstance(pragma_line, int) + pygmented_source_file_line = assert_cast( + pygmented_source_file_lines[pragma_line - 1], str + ) if isinstance(pragma, ForwardRangeMarker): + before_line = pygmented_source_file_line.rstrip("\n") + " " pygmented_source_file_lines[pragma_line - 1] = ( - pygmented_source_file_lines[pragma_line - 1].rstrip("\n") - + " ", - "\n", - pragma, + SourceMarkerTuple(Markup(before_line), Markup("\n"), pragma) ) continue @@ -202,7 +212,7 @@ def get_pygmented_source_lines( assert closing_bracket_index is not None after_line = source_line[closing_bracket_index:].rstrip() - pygmented_source_file_lines[pragma_line - 1] = ( + pygmented_source_file_lines[pragma_line - 1] = SourceMarkerTuple( escape(before_line), escape(after_line), pragma, @@ -212,6 +222,13 @@ def get_pygmented_source_lines( + html_formatter.get_style_defs(".highlight") ) - return list(map(Markup, pygmented_source_file_lines)), Markup( - pygments_styles - ) + return [ + SourceFileViewHTMLGenerator.mark_safe(line) + for line in pygmented_source_file_lines + ], Markup(pygments_styles) + + @staticmethod + def mark_safe( + line: Union[str, SourceMarkerTuple], + ) -> Union[Markup, SourceMarkerTuple]: + return line if isinstance(line, SourceMarkerTuple) else Markup(line) diff --git a/strictdoc/export/html/generators/view_objects/source_file_view_object.py b/strictdoc/export/html/generators/view_objects/source_file_view_object.py index 9ff0dbbb8..d5dea0060 100644 --- a/strictdoc/export/html/generators/view_objects/source_file_view_object.py +++ b/strictdoc/export/html/generators/view_objects/source_file_view_object.py @@ -1,11 +1,16 @@ # mypy: disable-error-code="no-any-return,no-untyped-call,no-untyped-def,union-attr" from dataclasses import dataclass from datetime import datetime -from typing import List +from typing import List, NamedTuple, Union from markupsafe import Markup from strictdoc import __version__ +from strictdoc.backend.sdoc_source_code.models.range_marker import ( + ForwardRangeMarker, + LineMarker, + RangeMarker, +) from strictdoc.core.document_tree_iterator import DocumentTreeIterator from strictdoc.core.finders.source_files_finder import SourceFile from strictdoc.core.project_config import ProjectConfig @@ -15,6 +20,18 @@ from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer +class SourceMarkerTuple(NamedTuple): + before_line: Markup + after_line: Markup + pragma: Union[ForwardRangeMarker, LineMarker, RangeMarker] + + +SourceLineEntry = Union[ + Markup, + SourceMarkerTuple, +] + + @dataclass class SourceFileViewObject: def __init__( @@ -26,7 +43,7 @@ def __init__( markup_renderer: MarkupRenderer, source_file: SourceFile, pygments_styles: Markup, - pygmented_source_file_lines: List[Markup], + pygmented_source_file_lines: List[SourceLineEntry], ): self.traceability_index: TraceabilityIndex = traceability_index self.project_config: ProjectConfig = project_config @@ -34,7 +51,7 @@ def __init__( self.markup_renderer: MarkupRenderer = markup_renderer self.source_file: SourceFile = source_file self.pygments_styles: Markup = pygments_styles - self.pygmented_source_file_lines: List[Markup] = ( + self.pygmented_source_file_lines: List[SourceLineEntry] = ( pygmented_source_file_lines ) diff --git a/strictdoc/export/html/templates/screens/source_file_view/main.jinja b/strictdoc/export/html/templates/screens/source_file_view/main.jinja index 1e627a751..52a5696ae 100644 --- a/strictdoc/export/html/templates/screens/source_file_view/main.jinja +++ b/strictdoc/export/html/templates/screens/source_file_view/main.jinja @@ -6,7 +6,7 @@ {%- for line in view_object.pygmented_source_file_lines -%}
{{ loop.index }}
- {%- if line.__class__.__name__ == "tuple" -%} + {%- if line.__class__.__name__ == "SourceMarkerTuple" -%} {%- set replacement_before, replacement_after, pragma = line -%} {# Note: Cannot format HTML/Jinja blocks within 'pre' tags! #}
{{ replacement_before }}{%- for req in pragma.reqs_objs -%}
diff --git a/tests/integration/commands/export/html/escaping/01_escape_input_from_sdoc/input.sdoc b/tests/integration/commands/export/html/escaping/01_escape_input_from_sdoc/input.sdoc
new file mode 100644
index 000000000..ccd9a4f2d
--- /dev/null
+++ b/tests/integration/commands/export/html/escaping/01_escape_input_from_sdoc/input.sdoc
@@ -0,0 +1,18 @@
+[DOCUMENT]
+TITLE: "escaping" 'document title'
+
+[SECTION]
+TITLE: "escaping" 'section title'
+
+[TEXT]
+STATEMENT: >>>
+"escaping" 'text statement'
+<<<
+
+[REQUIREMENT]
+UID: REQ-1
+STATEMENT: >>>
+"escaping" 'requirement statement'
+<<<
+
+[/SECTION]
diff --git a/tests/integration/commands/export/html/escaping/01_escape_input_from_sdoc/strictdoc.toml b/tests/integration/commands/export/html/escaping/01_escape_input_from_sdoc/strictdoc.toml
new file mode 100644
index 000000000..24a5dae1f
--- /dev/null
+++ b/tests/integration/commands/export/html/escaping/01_escape_input_from_sdoc/strictdoc.toml
@@ -0,0 +1,8 @@
+[project]
+
+features = [
+  "DEEP_TRACEABILITY_SCREEN",
+  "STANDALONE_DOCUMENT_SCREEN",
+  "TABLE_SCREEN",
+  "TRACEABILITY_SCREEN",
+]
diff --git a/tests/integration/commands/export/html/escaping/01_escape_input_from_sdoc/test.itest b/tests/integration/commands/export/html/escaping/01_escape_input_from_sdoc/test.itest
new file mode 100644
index 000000000..eb1d42d4c
--- /dev/null
+++ b/tests/integration/commands/export/html/escaping/01_escape_input_from_sdoc/test.itest
@@ -0,0 +1,51 @@
+RUN: %strictdoc export %S --output-dir Output/ | filecheck %s --dump-input=fail --check-prefix CHECK-EXPORT
+CHECK-EXPORT: Published: "escaping" 'document title'
+
+RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input.html | filecheck %s --dump-input=fail --check-prefix CHECK-ALL
+RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input.html | filecheck %s --dump-input=fail --check-prefix CHECK-INPUT
+RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input-DEEP-TRACE.html | filecheck %s --dump-input=fail --check-prefix CHECK-ALL
+RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input-DEEP-TRACE.html | filecheck %s --dump-input=fail --check-prefix CHECK-DTR
+RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input-TABLE.html | filecheck %s --dump-input=fail --check-prefix CHECK-ALL
+RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input-TABLE.html | filecheck %s --dump-input=fail --check-prefix CHECK-TABLE
+RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input-TRACE.html | filecheck %s --dump-input=fail --check-prefix CHECK-ALL
+RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input-TRACE.html | filecheck %s --dump-input=fail --check-prefix CHECK-TRACE
+
+# Browser title bar: Document title.
+CHECK-ALL: <b>"escaping"&nbsp;'document title'</b> - {{Document|Deep Traceability|Table|Traceability}}
+
+# Left-hand bar: project tree document entry.
+CHECK-ALL: class="document_title"
+CHECK-ALL-NEXT: title="<b>"escaping"&nbsp;'document title'</b>"
+CHECK-ALL-NEXT: data-file_name="input.sdoc"
+CHECK-ALL-NEXT: ><b>"escaping"&nbsp;'document title'</b>
+ +# Right-hand bar: Document TOC. +CHECK-ALL: +CHECK-ALL-NEXT: {{[0-9]+}} +CHECK-ALL-NEXT: <b>"escaping"&nbsp;'section title'</b> + +# Header: Document path in tree. +CHECK-ALL: class="header__document_title" +CHECK-ALL-NEXT: title="<b>"escaping"&nbsp;'document title'</b>" +CHECK-ALL-NEXT: > +CHECK-ALL-NEXT: <b>"escaping"&nbsp;'document title'</b> + +# Main document: Title. +CHECK-INPUT:

<b>"escaping"&nbsp;'document title'</b>

+CHECK-TABLE:

<b>"escaping"&nbsp;'document title'</b>

+ +# Main document: Section. +CHECK-INPUT: 1. <b>"escaping"&nbsp;'section title'</b> +CHECK-DTR: 1. <b>"escaping"&nbsp;'section title'</b> +CHECK-TABLE:
+CHECK-TABLE-NEXT: <b>"escaping"&nbsp;'section title'</b> +CHECK-TABLE-NEXT:
+CHECK-TRACE: 1. <b>"escaping"&nbsp;'section title'</b> + +# Main document: Text statement. +CHECK-INPUT:

<b>"escaping"&nbsp;'text statement'</b>

+CHECK-TABLE:

<b>"escaping"&nbsp;'text statement'</b>

+CHECK-TRACE:

<b>"escaping"&nbsp;'text statement'</b>

+ +# Main document: Requirement statement. +CHECK-ALL:

<b>"escaping"&nbsp;'requirement statement'</b>

diff --git a/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/file.py b/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/file.py new file mode 100644 index 000000000..224963ac4 --- /dev/null +++ b/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/file.py @@ -0,0 +1,15 @@ +# "escaping" 'line mark' @sdoc(REQ-1) +def print_test(): + test1 = """ + "escaping" 'normal src line' + """ + test2 = """"escaping" 'forward range mark before' + "escaping" 'forward range mark after' + """ + print(f"{test1} {test2}") # noqa: T201# + + +# "escaping" 'range mark before' @sdoc[REQ-1] +def hello_world(): + print("hello world") # noqa: T201 +# "escaping" 'range mark after' @sdoc[/REQ-1] diff --git a/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/input.sdoc b/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/input.sdoc new file mode 100644 index 000000000..67042fa1a --- /dev/null +++ b/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/input.sdoc @@ -0,0 +1,14 @@ +[DOCUMENT] +TITLE: HTML escaping of source file content + +[REQUIREMENT] +UID: REQ-1 +STATEMENT: >>> +Source files are external input, their content must be HTML escaped. +<<< +RELATIONS: +- TYPE: File + VALUE: file.py +- TYPE: File + VALUE: file.py + LINE_RANGE: 6, 7 diff --git a/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/strictdoc.toml b/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/strictdoc.toml new file mode 100644 index 000000000..943e69f5a --- /dev/null +++ b/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/strictdoc.toml @@ -0,0 +1,5 @@ +[project] + +features = [ + "REQUIREMENT_TO_SOURCE_TRACEABILITY", +] diff --git a/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/test.itest b/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/test.itest new file mode 100644 index 000000000..4f96dc0f6 --- /dev/null +++ b/tests/integration/commands/export/html/escaping/02_escape_input_from_src_file/test.itest @@ -0,0 +1,59 @@ +RUN: %strictdoc export %S --output-dir Output/ | filecheck %s --dump-input=fail --check-prefix CHECK-EXPORT +CHECK-EXPORT: Published: HTML escaping of source file content + +RUN: %cat %S/Output/html/_source_files/file.py.html | filecheck %s --dump-input=fail --check-prefix CHECK-SRC + +# Line marker. +CHECK-SRC:
# <b>"escaping"&nbsp;'line mark'</b> @sdoc(REQ-1)
2
+ +# Normal source code line. +CHECK-SRC:
{{\s+}}<b>"escaping"&nbsp;'normal src line'</b>
+ +# Forward range marker before. +CHECK-SRC:
{{\s+}}test2{{\s+}}= """<b>"escaping"&nbsp;'forward range mark before'</b>{{\s+}}REQ-1
+CHECK-SRC-NEXT: 
+ +# Forward range marker after. +CHECK-SRC:
{{\s+}}<b>"escaping"&nbsp;'forward range mark after'</b>{{\s+}}/REQ-1
+CHECK-SRC-NEXT: 
+ +# Range marker before. +CHECK-SRC:
# <b>"escaping"&nbsp;'range mark before'</b> @sdoc[REQ-1]
+ +# Range marker after. +CHECK-SRC:
# <b>"escaping"&nbsp;'range mark after'</b> @sdoc[/REQ-1]