diff --git a/docs/strictdoc_25_design.sdoc b/docs/strictdoc_25_design.sdoc index 1e17efb2e..c8ce20130 100644 --- a/docs/strictdoc_25_design.sdoc +++ b/docs/strictdoc_25_design.sdoc @@ -136,3 +136,39 @@ One important implementation detail of Arpeggio that influences StrictDoc user e [/FREETEXT] [/SECTION] + +[SECTION] +TITLE: HTML Escaping + +[TEXT] +STATEMENT: >>> +StrictDoc uses Jinja2 autoescaping_ for HTML output. `Template.render`_ calls +will escape any Python object unless it's explicitly marked as safe. + +Good to know for a start: + +- If a Python object intentionally contains HTML it must be marked as safe + to bypass autoescaping. Templates can do this by piping to safe_, or Python code + can do it by wrapping an object into `markupsafe.Markup`_. +- Passing text to the `Markup() `_ constructor marks that text + as safe, but *does not escape* it. +- Text can be explicitly escaped with `markupsafe.escape`_. It's similar to + `html.escape`_, but the result is immediately marked safe. +- `markupsafe.Markup`_ is responsible for some "magic". It's a :code:`str` subclass + with the same methods, but escaping arguments. For example, + :code:`"> " + Markup("
safe
")` will turn into :code:`">
safe
"`, + thanks to :code:`__radd__` in this specific case. To prevent escaping, + you would use :code:`Markup("> ") + Markup("
safe
")`. Basically the + same magic happens in templates when using safe_. +- See also `Working with Automatic Escaping`_. + +.. _autoescaping: https://jinja.palletsprojects.com/en/latest/api/#autoescaping +.. _Working with Automatic Escaping: https://jinja.palletsprojects.com/en/latest/templates/#working-with-automatic-escaping +.. _markupsafe.Markup: https://markupsafe.palletsprojects.com/en/latest/escaping/#markupsafe.Markup +.. _markupsafe.escape: https://markupsafe.palletsprojects.com/en/latest/escaping/#markupsafe.escape +.. _safe: https://jinja.palletsprojects.com/en/latest/templates/#jinja-filters.safe +.. _Template.render: https://jinja.palletsprojects.com/en/latest/api/#jinja2.Template.render +.. _html.escape: https://docs.python.org/3/library/html.html#html.escape +<<< + +[/SECTION] diff --git a/strictdoc/backend/sdoc/models/free_text.py b/strictdoc/backend/sdoc/models/free_text.py index 5d4054fa1..9723218bb 100644 --- a/strictdoc/backend/sdoc/models/free_text.py +++ b/strictdoc/backend/sdoc/models/free_text.py @@ -1,4 +1,3 @@ -import html from typing import Any, List, Optional from strictdoc.backend.sdoc.models.anchor import Anchor @@ -55,9 +54,6 @@ def get_parts_as_text(self) -> str: raise NotImplementedError(part) return text - def get_parts_as_text_escaped(self) -> str: - return html.escape(self.get_parts_as_text()) - class FreeTextContainer(FreeText): def __init__(self, parts: List[Any]) -> None: diff --git a/strictdoc/backend/sdoc/models/node.py b/strictdoc/backend/sdoc/models/node.py index 3cb99e824..dd77427c7 100644 --- a/strictdoc/backend/sdoc/models/node.py +++ b/strictdoc/backend/sdoc/models/node.py @@ -1,5 +1,4 @@ # mypy: disable-error-code="union-attr" -import html from collections import OrderedDict from typing import Any, Generator, List, Optional, Tuple, Union @@ -92,9 +91,6 @@ def get_text_value(self) -> str: raise NotImplementedError(part) return text - def get_text_value_escaped(self) -> str: - return html.escape(self.get_text_value()) - @auto_described class SDocNode(SDocObject): @@ -456,13 +452,6 @@ def enumerate_all_fields( meta_field_value = field.get_text_value() yield field, field.field_name, meta_field_value - def enumerate_all_fields_escaped( - self, - ) -> Generator[Tuple[SDocNodeField, str, str], None, None]: - for field in self.enumerate_fields(): - meta_field_value = field.get_text_value_escaped() - yield field, field.field_name, meta_field_value - def enumerate_meta_fields( self, skip_single_lines: bool = False, skip_multi_lines: bool = False ) -> Generator[Tuple[str, SDocNodeField], None, None]: diff --git a/strictdoc/core/transforms/create_requirement.py b/strictdoc/core/transforms/create_requirement.py index 2a2e617ce..2ee0db079 100644 --- a/strictdoc/core/transforms/create_requirement.py +++ b/strictdoc/core/transforms/create_requirement.py @@ -108,14 +108,14 @@ def perform(self): ) = RstToHtmlFragmentWriter( path_to_output_dir=self.project_config.export_output_dir, context_document=document, - ).write_with_validation(field_.field_unescaped_value) + ).write_with_validation(field_.field_value) if parsed_html is None: errors[field_.field_name].append(rst_error) else: try: free_text_container: Optional[FreeTextContainer] = ( - SDFreeTextReader.read(field_.field_unescaped_value) - if len(field_.field_unescaped_value) > 0 + SDFreeTextReader.read(field_.field_value) + if len(field_.field_value) > 0 else None ) map_form_to_requirement_fields[field_] = ( @@ -208,7 +208,7 @@ def perform(self): requirement.set_field_value( field_name=form_field_name, form_field_index=form_field_index, - value=form_field.field_unescaped_value, + value=form_field.field_value, ) continue diff --git a/strictdoc/core/transforms/update_requirement.py b/strictdoc/core/transforms/update_requirement.py index 6bbae3501..2d217e877 100644 --- a/strictdoc/core/transforms/update_requirement.py +++ b/strictdoc/core/transforms/update_requirement.py @@ -114,14 +114,14 @@ def perform(self): ) = RstToHtmlFragmentWriter( path_to_output_dir=self.project_config.export_output_dir, context_document=self.context_document, - ).write_with_validation(field_.field_unescaped_value) + ).write_with_validation(field_.field_value) if parsed_html is None: form_object.add_error(field_.field_name, rst_error) else: try: free_text_container: Optional[FreeTextContainer] = ( - SDFreeTextReader.read(field_.field_unescaped_value) - if len(field_.field_unescaped_value) > 0 + SDFreeTextReader.read(field_.field_value) + if len(field_.field_value) > 0 else None ) map_form_to_requirement_fields[field_] = ( @@ -455,7 +455,7 @@ def populate_node_fields_from_form_object( node.set_field_value( field_name=form_field_name, form_field_index=form_field_index, - value=form_field.field_unescaped_value, + value=form_field.field_value, ) continue diff --git a/strictdoc/export/html/form_objects/form_object.py b/strictdoc/export/html/form_objects/form_object.py index 506251e67..d8391124a 100644 --- a/strictdoc/export/html/form_objects/form_object.py +++ b/strictdoc/export/html/form_objects/form_object.py @@ -2,25 +2,25 @@ from dataclasses import dataclass from typing import Any, Dict, List -from jinja2 import Environment, Template +from strictdoc.export.html.html_templates import JinjaEnvironment @dataclass class RowWithReservedFieldFormObject: field: Any errors: Dict[str, List] - jinja_environment: Environment + jinja_environment: JinjaEnvironment def __post_init__(self): assert isinstance( - self.jinja_environment, Environment + self.jinja_environment, JinjaEnvironment ), self.jinja_environment def render(self): - template: Template = self.jinja_environment.get_template( - "components/grammar_form_element/row_with_reserved_field/index.jinja" + rendered_template = self.jinja_environment.render_template_as_markup( + "components/grammar_form_element/row_with_reserved_field/index.jinja", + form_object=self, ) - rendered_template = template.render(form_object=self) return rendered_template def get_errors(self, field_name) -> List: @@ -31,19 +31,19 @@ def get_errors(self, field_name) -> List: class RowWithCustomFieldFormObject: field: Any errors: Dict[str, List] - jinja_environment: Environment + jinja_environment: JinjaEnvironment def __post_init__(self): assert self.field is not None assert isinstance( - self.jinja_environment, Environment + self.jinja_environment, JinjaEnvironment ), self.jinja_environment def render(self): - template: Template = self.jinja_environment.get_template( - "components/grammar_form_element/row_with_custom_field/index.jinja" + rendered_template = self.jinja_environment.render_template_as_markup( + "components/grammar_form_element/row_with_custom_field/index.jinja", + form_object=self, ) - rendered_template = template.render(form_object=self) return rendered_template def get_errors(self, field_name) -> List: @@ -54,19 +54,19 @@ def get_errors(self, field_name) -> List: class RowWithRelationFormObject: relation: Any errors: Dict[str, List] - jinja_environment: Environment + jinja_environment: JinjaEnvironment def __post_init__(self): assert self.relation is not None assert isinstance( - self.jinja_environment, Environment + self.jinja_environment, JinjaEnvironment ), self.jinja_environment def render(self): - template: Template = self.jinja_environment.get_template( - "components/grammar_form_element/row_with_relation/index.jinja" + rendered_template = self.jinja_environment.render_template_as_markup( + "components/grammar_form_element/row_with_relation/index.jinja", + form_object=self, ) - rendered_template = template.render(form_object=self) return rendered_template def get_errors(self, field_name) -> List: diff --git a/strictdoc/export/html/form_objects/grammar_element_form_object.py b/strictdoc/export/html/form_objects/grammar_element_form_object.py index bf74916c2..d9f510ae1 100644 --- a/strictdoc/export/html/form_objects/grammar_element_form_object.py +++ b/strictdoc/export/html/form_objects/grammar_element_form_object.py @@ -1,7 +1,7 @@ # mypy: disable-error-code="arg-type,no-any-return,no-untyped-call,no-untyped-def,union-attr,type-arg" from typing import Dict, List, Optional, Set, Tuple, Union -from jinja2 import Environment, Template +from jinja2 import Template from starlette.datastructures import FormData from strictdoc.backend.sdoc.models.document import SDocDocument @@ -23,6 +23,7 @@ RowWithRelationFormObject, RowWithReservedFieldFormObject, ) +from strictdoc.export.html.html_templates import JinjaEnvironment from strictdoc.helpers.auto_described import auto_described from strictdoc.helpers.cast import assert_cast from strictdoc.helpers.form_data import parse_form_data @@ -108,7 +109,7 @@ def __init__( fields: List[GrammarFormField], relations: List[GrammarFormRelation], project_config: ProjectConfig, - jinja_environment: Environment, + jinja_environment: JinjaEnvironment, ): assert isinstance(document_mid, str), document_mid super().__init__() @@ -118,7 +119,7 @@ def __init__( self.fields: List[GrammarFormField] = fields self.relations: List[GrammarFormRelation] = relations self.project_config: ProjectConfig = project_config - self.jinja_environment: Environment = jinja_environment + self.jinja_environment: JinjaEnvironment = jinja_environment @staticmethod def create_from_request( @@ -126,7 +127,7 @@ def create_from_request( document: SDocDocument, request_form_data: FormData, project_config: ProjectConfig, - jinja_environment: Environment, + jinja_environment: JinjaEnvironment, ) -> "GrammarElementFormObject": form_object_fields: List[GrammarFormField] = [] form_object_relations: List[GrammarFormRelation] = [] @@ -193,7 +194,7 @@ def create_from_document( document: SDocDocument, element_mid: str, project_config: ProjectConfig, - jinja_environment: Environment, + jinja_environment: JinjaEnvironment, ) -> "GrammarElementFormObject": assert isinstance(document, SDocDocument) assert isinstance(document.grammar, DocumentGrammar) @@ -371,10 +372,9 @@ def render(self): ) def render_after_validation(self): - template: Template = self.jinja_environment.get_template( - "components/grammar_form_element/index.jinja" + rendered_template = self.jinja_environment.render_template_as_markup( + "components/grammar_form_element/index.jinja", form_object=self ) - rendered_template = template.render(form_object=self) return render_turbo_stream( content=rendered_template, action="update", target="modal" ) diff --git a/strictdoc/export/html/form_objects/grammar_form_object.py b/strictdoc/export/html/form_objects/grammar_form_object.py index 01d07d6fb..aa9801b96 100644 --- a/strictdoc/export/html/form_objects/grammar_form_object.py +++ b/strictdoc/export/html/form_objects/grammar_form_object.py @@ -1,7 +1,6 @@ # mypy: disable-error-code="arg-type,no-any-return,no-untyped-call,no-untyped-def,type-arg" from typing import Dict, List, Optional, Set -from jinja2 import Environment, Template from starlette.datastructures import FormData from strictdoc.backend.sdoc.models.document import SDocDocument @@ -13,6 +12,7 @@ from strictdoc.export.html.form_objects.rows.row_with_grammar_element_form_object import ( RowWithGrammarElementFormObject, ) +from strictdoc.export.html.html_templates import JinjaEnvironment from strictdoc.helpers.auto_described import auto_described from strictdoc.helpers.cast import assert_cast from strictdoc.helpers.form_data import parse_form_data @@ -68,7 +68,7 @@ def __init__( document_mid: str, fields: List[GrammarElementFormField], project_config: ProjectConfig, - jinja_environment: Environment, + jinja_environment: JinjaEnvironment, imported_grammar_file: Optional[str], ): assert isinstance(document_mid, str), document_mid @@ -76,7 +76,7 @@ def __init__( self.document_mid = document_mid self.fields: List[GrammarElementFormField] = fields self.project_config: ProjectConfig = project_config - self.jinja_environment: Environment = jinja_environment + self.jinja_environment: JinjaEnvironment = jinja_environment self.imported_grammar_file: Optional[str] = imported_grammar_file @staticmethod @@ -85,7 +85,7 @@ def create_from_request( document_mid: str, request_form_data: FormData, project_config: ProjectConfig, - jinja_environment: Environment, + jinja_environment: JinjaEnvironment, ) -> "GrammarFormObject": form_object_fields: List[GrammarElementFormField] = [] request_form_data_as_list = [ @@ -125,7 +125,7 @@ def create_from_document( *, document: SDocDocument, project_config: ProjectConfig, - jinja_environment: Environment, + jinja_environment: JinjaEnvironment, ) -> "GrammarFormObject": assert isinstance(document, SDocDocument) assert isinstance(document.grammar, DocumentGrammar) @@ -180,10 +180,9 @@ def validate(self) -> bool: return len(self.errors) == 0 def render(self): - template: Template = self.jinja_environment.get_template( - "components/grammar_form/index.jinja" + rendered_template = self.jinja_environment.render_template_as_markup( + "components/grammar_form/index.jinja", form_object=self ) - rendered_template = template.render(form_object=self) return render_turbo_stream( content=rendered_template, action="update", target="modal" ) diff --git a/strictdoc/export/html/form_objects/included_document_form_object.py b/strictdoc/export/html/form_objects/included_document_form_object.py index 368ad1ce4..be94b0fc1 100644 --- a/strictdoc/export/html/form_objects/included_document_form_object.py +++ b/strictdoc/export/html/form_objects/included_document_form_object.py @@ -3,10 +3,10 @@ from collections import defaultdict from typing import Dict, List, Optional -from jinja2 import Environment, Template from starlette.datastructures import FormData from strictdoc.backend.sdoc.models.document import SDocDocument +from strictdoc.export.html.html_templates import JinjaEnvironment from strictdoc.helpers.auto_described import auto_described from strictdoc.helpers.cast import assert_cast from strictdoc.helpers.form_data import parse_form_data @@ -23,7 +23,7 @@ def __init__( document_mid: str, context_document_mid: str, document_title: str, - jinja_environment: Environment, + jinja_environment: JinjaEnvironment, ): assert isinstance(document_mid, str), document_mid assert isinstance(context_document_mid, str), context_document_mid @@ -32,11 +32,11 @@ def __init__( self.document_mid: Optional[str] = document_mid self.context_document_mid: Optional[str] = context_document_mid self.document_title: str = document_title - self.jinja_environment: Environment = jinja_environment + self.jinja_environment: JinjaEnvironment = jinja_environment @staticmethod def create_from_request( - *, request_form_data: FormData, jinja_environment: Environment + *, request_form_data: FormData, jinja_environment: JinjaEnvironment ) -> "IncludedDocumentFormObject": request_form_data_as_list = [ (field_name, field_value) @@ -76,7 +76,7 @@ def create_from_document( *, document: SDocDocument, context_document_mid: str, - jinja_environment: Environment, + jinja_environment: JinjaEnvironment, ) -> "IncludedDocumentFormObject": assert isinstance(document, SDocDocument) @@ -88,10 +88,9 @@ def create_from_document( ) def render_edit_form(self): - template: Template = self.jinja_environment.get_template( - "components/included_document_form/index.jinja" + rendered_template = self.jinja_environment.render_template_as_markup( + "components/included_document_form/index.jinja", form_object=self ) - rendered_template = template.render(form_object=self) return render_turbo_stream( content=rendered_template, action="replace", diff --git a/strictdoc/export/html/form_objects/requirement_form_object.py b/strictdoc/export/html/form_objects/requirement_form_object.py index 650a624d3..c0c818519 100644 --- a/strictdoc/export/html/form_objects/requirement_form_object.py +++ b/strictdoc/export/html/form_objects/requirement_form_object.py @@ -1,5 +1,4 @@ # mypy: disable-error-code="arg-type,attr-defined,no-redef,no-untyped-call,no-untyped-def,union-attr,type-arg" -import html from collections import defaultdict from enum import Enum from typing import Dict, List, Optional, Set, Union @@ -57,15 +56,12 @@ def __init__( field_mid: str, field_name: str, field_type: RequirementFormFieldType, - field_unescaped_value: str, - field_escaped_value: str, + field_value: str, ): - assert isinstance(field_unescaped_value, str) - assert isinstance(field_escaped_value, str) + assert isinstance(field_value, str) self.field_mid: str = field_mid self.field_name: str = field_name - self.field_unescaped_value: str = field_unescaped_value - self.field_escaped_value: str = field_escaped_value + self.field_value: str = field_value self.field_type = field_type def is_singleline(self): @@ -85,18 +81,12 @@ def create_from_grammar_field( *, grammar_field: GrammarElementField, multiline: bool, - value_unescaped: str, - value_escaped: str, + value: str, ) -> "RequirementFormField": - assert isinstance(value_unescaped, str), ( - grammar_field, - multiline, - value_unescaped, - ) - assert isinstance(value_escaped, str), ( + assert isinstance(value, str), ( grammar_field, multiline, - value_escaped, + value, ) if grammar_field.gef_type in ( RequirementFieldType.STRING, @@ -111,8 +101,7 @@ def create_from_grammar_field( if multiline else RequirementFormFieldType.SINGLELINE ), - field_unescaped_value=value_unescaped, - field_escaped_value=value_escaped, + field_value=value, ) raise NotImplementedError(grammar_field) @@ -128,7 +117,6 @@ def create_existing_from_grammar_field( RequirementFieldType.MULTIPLE_CHOICE, ): field_value = requirement_field.get_text_value() - escaped_field_value = html.escape(field_value) return RequirementFormField( field_mid=MID.create(), field_name=grammar_field.title, @@ -137,8 +125,7 @@ def create_existing_from_grammar_field( if multiline else RequirementFormFieldType.SINGLELINE ), - field_unescaped_value=field_value, - field_escaped_value=escaped_field_value, + field_value=field_value, ) raise NotImplementedError(grammar_field) @@ -148,8 +135,7 @@ def create_mid_field(mid: MID) -> "RequirementFormField": field_mid=MID.create(), field_name="MID", field_type=RequirementFormFieldType.SINGLELINE, - field_unescaped_value=mid, - field_escaped_value=html.escape(mid), + field_value=mid, ) @@ -327,8 +313,7 @@ def create_from_request( form_field = RequirementFormField.create_from_grammar_field( grammar_field=field, multiline=multiline, - value_unescaped=sanitized_field_value, - value_escaped=html.escape(sanitized_field_value), + value=sanitized_field_value, ) form_fields.append(form_field) @@ -381,21 +366,14 @@ def create_new( RequirementFormField.create_from_grammar_field( grammar_field=field, multiline=field_idx >= content_field_idx, - value_unescaped="", - value_escaped="", + value="", ) ) form_fields.append(form_field) if form_field.field_name == "UID" and next_uid is not None: - form_field.field_unescaped_value = next_uid - form_field.field_escaped_value = next_uid + form_field.field_value = next_uid elif form_field.field_name == "MID" and document.config.enable_mid: - form_field.field_unescaped_value = ( - new_requirement_mid.get_string_value() - ) - form_field.field_escaped_value = ( - new_requirement_mid.get_string_value() - ) + form_field.field_value = new_requirement_mid.get_string_value() return RequirementFormObject( is_new=True, @@ -455,8 +433,7 @@ def create_from_requirement( form_field = RequirementFormField.create_from_grammar_field( grammar_field=field, multiline=multiline, - value_unescaped="", - value_escaped="", + value="", ) form_fields.append(form_field) @@ -515,8 +492,7 @@ def clone_from_requirement( for field_name, fields_ in form_object.fields.items(): if field_name == "UID": field: RequirementFormField = fields_[0] - field.field_unescaped_value = clone_uid - field.field_escaped_value = clone_uid + field.field_value = clone_uid form_object.requirement_mid = MID.create() return form_object @@ -613,7 +589,7 @@ def validate( FIXME: MID uniqueness if a node is updated. """ if self.is_new and "MID" in self.fields: - new_node_mid = self.fields["MID"][0].field_unescaped_value + new_node_mid = self.fields["MID"][0].field_value if len(new_node_mid) > 0: existing_node_with_this_mid = ( traceability_index.get_node_by_mid_weak(MID(new_node_mid)) @@ -633,7 +609,7 @@ def validate( """ new_node_uid_or_none: Optional[str] = None if "UID" in self.fields: - new_node_uid = self.fields["UID"][0].field_unescaped_value + new_node_uid = self.fields["UID"][0].field_value if len(new_node_uid) > 0: new_node_uid_or_none = new_node_uid @@ -689,9 +665,7 @@ def validate( """ requirement_element = self.grammar.elements_by_type[self.element_type] statement_field_name = requirement_element.content_field[0] - requirement_statement = self.fields[statement_field_name][ - 0 - ].field_unescaped_value + requirement_statement = self.fields[statement_field_name][0].field_value if requirement_statement is None or len(requirement_statement) == 0: self.add_error( statement_field_name, @@ -717,7 +691,7 @@ def validate( and grammar_element_field_.required ): for form_field_ in self.fields[grammar_element_field_.title]: - field_value = form_field_.field_unescaped_value + field_value = form_field_.field_value if field_value is None or len(field_value) == 0: self.add_error( grammar_element_field_.title, @@ -735,9 +709,7 @@ def validate( self._validate_choice(grammar_element_field_) requirement_uid: Optional[str] = ( - self.fields["UID"][0].field_unescaped_value - if "UID" in self.fields - else None + self.fields["UID"][0].field_value if "UID" in self.fields else None ) if len(self.reference_fields) > 0 and ( requirement_uid is None or len(requirement_uid) == 0 @@ -862,10 +834,7 @@ def child_lambda(requirement_id_) -> List[str]: def _validate_choice(self, grammar_element_field: GrammarElementField): field_0 = self.fields[grammar_element_field.title][0] - if ( - len(field_0.field_unescaped_value) == 0 - and not grammar_element_field.required - ): + if len(field_0.field_value) == 0 and not grammar_element_field.required: # The empty choice fields are allowed if the field is not REQUIRED. return @@ -881,8 +850,7 @@ def _validate_choice(self, grammar_element_field: GrammarElementField): ) if ( grammar_element_field.gef_type == RequirementFieldType.SINGLE_CHOICE - and field_0.field_unescaped_value - not in choice_grammar_element_field.options + and field_0.field_value not in choice_grammar_element_field.options ): self.add_error( grammar_element_field.title, @@ -896,14 +864,13 @@ def _validate_choice(self, grammar_element_field: GrammarElementField): == RequirementFieldType.MULTIPLE_CHOICE ): choices = [ - choice.strip() - for choice in field_0.field_unescaped_value.split(",") + choice.strip() for choice in field_0.field_value.split(",") ] if all( choice in choice_grammar_element_field.options for choice in choices ): - field_0.field_unescaped_value = ", ".join(choices) + field_0.field_value = ", ".join(choices) else: self.add_error( grammar_element_field.title, diff --git a/strictdoc/export/html/form_objects/rows/row_with_grammar_element_form_object.py b/strictdoc/export/html/form_objects/rows/row_with_grammar_element_form_object.py index a604622e2..b8143ed9b 100644 --- a/strictdoc/export/html/form_objects/rows/row_with_grammar_element_form_object.py +++ b/strictdoc/export/html/form_objects/rows/row_with_grammar_element_form_object.py @@ -2,33 +2,33 @@ from dataclasses import dataclass from typing import Any, Dict, List -from jinja2 import Environment, Template +from strictdoc.export.html.html_templates import JinjaEnvironment @dataclass class RowWithGrammarElementFormObject: field: Any errors: Dict[str, List] - jinja_environment: Environment + jinja_environment: JinjaEnvironment def __post_init__(self): assert self.field is not None assert isinstance( - self.jinja_environment, Environment + self.jinja_environment, JinjaEnvironment ), self.jinja_environment def render(self): if self.field.is_new: - template: Template = self.jinja_environment.get_template( - "components/grammar_form/row_with_new_grammar_element/index.jinja" + rendered_template = self.jinja_environment.render_template_as_markup( + "components/grammar_form/row_with_new_grammar_element/index.jinja", + form_object=self, ) - rendered_template = template.render(form_object=self) return rendered_template else: - template: Template = self.jinja_environment.get_template( - "components/grammar_form/row_with_grammar_element/index.jinja" + rendered_template = self.jinja_environment.render_template_as_markup( + "components/grammar_form/row_with_grammar_element/index.jinja", + form_object=self, ) - rendered_template = template.render(form_object=self) return rendered_template def get_errors(self, field_name) -> List: diff --git a/strictdoc/export/html/form_objects/section_form_object.py b/strictdoc/export/html/form_objects/section_form_object.py index b24748547..4c65edc91 100644 --- a/strictdoc/export/html/form_objects/section_form_object.py +++ b/strictdoc/export/html/form_objects/section_form_object.py @@ -1,5 +1,4 @@ # mypy: disable-error-code="arg-type,no-untyped-call,no-untyped-def,type-arg" -import html from collections import defaultdict from typing import Dict @@ -41,11 +40,11 @@ def __init__( @property def section_uid(self): - return self.section_uid_field.field_unescaped_value + return self.section_uid_field.field_value @property def section_title(self): - return self.section_title_field.field_unescaped_value + return self.section_title_field.field_value @staticmethod def create_new(context_document_mid: str): @@ -55,15 +54,13 @@ def create_new(context_document_mid: str): field_mid=MID.create(), field_name="UID", field_type=RequirementFormFieldType.SINGLELINE, - field_unescaped_value="", - field_escaped_value="", + field_value="", ), section_title_field=RequirementFormField( field_mid=MID.create(), field_name="TITLE", field_type=RequirementFormFieldType.SINGLELINE, - field_unescaped_value="", - field_escaped_value="", + field_value="", ), context_document_mid=context_document_mid, ) @@ -73,10 +70,7 @@ def create_from_section(*, section: SDocSection, context_document_mid: str): uid_field_value = ( section.reserved_uid if section.reserved_uid is not None else "" ) - uid_escaped_field_value = html.escape(uid_field_value) - title_field_value = section.title if section.title is not None else "" - title_escaped_field_value = html.escape(title_field_value) return SectionFormObject( section_mid=section.reserved_mid, @@ -84,15 +78,13 @@ def create_from_section(*, section: SDocSection, context_document_mid: str): field_mid=MID.create(), field_name="UID", field_type=RequirementFormFieldType.SINGLELINE, - field_unescaped_value=uid_field_value, - field_escaped_value=uid_escaped_field_value, + field_value=uid_field_value, ), section_title_field=RequirementFormField( field_mid=MID.create(), field_name="TITLE", field_type=RequirementFormFieldType.SINGLELINE, - field_unescaped_value=title_field_value, - field_escaped_value=title_escaped_field_value, + field_value=title_field_value, ), context_document_mid=context_document_mid, ) @@ -128,8 +120,7 @@ def create_from_request( field_mid=MID.create(), field_name="UID", field_type=RequirementFormFieldType.SINGLELINE, - field_unescaped_value=sanitized_uid_field_value, - field_escaped_value=html.escape(sanitized_uid_field_value), + field_value=sanitized_uid_field_value, ) title_field_value = requirement_fields["TITLE"][0] @@ -140,8 +131,7 @@ def create_from_request( field_mid=MID.create(), field_name="TITLE", field_type=RequirementFormFieldType.SINGLELINE, - field_unescaped_value=sanitized_title_field_value, - field_escaped_value=html.escape(sanitized_title_field_value), + field_value=sanitized_title_field_value, ) form_object = SectionFormObject( diff --git a/strictdoc/export/html/generators/source_file_coverage.py b/strictdoc/export/html/generators/source_file_coverage.py index cdcacc489..71aaf4ff7 100644 --- a/strictdoc/export/html/generators/source_file_coverage.py +++ b/strictdoc/export/html/generators/source_file_coverage.py @@ -1,10 +1,9 @@ # mypy: disable-error-code="no-untyped-call,no-untyped-def" -from jinja2 import Environment from strictdoc import __version__ from strictdoc.core.project_config import ProjectConfig from strictdoc.core.traceability_index import TraceabilityIndex -from strictdoc.export.html.html_templates import HTMLTemplates +from strictdoc.export.html.html_templates import HTMLTemplates, JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer @@ -24,11 +23,10 @@ def __init__( self.is_running_on_server: bool = project_config.is_running_on_server self.strictdoc_version = __version__ - def render_screen(self, jinja_environment: Environment): - template = jinja_environment.get_template( - "screens/source_file_coverage/index.jinja" + def render_screen(self, jinja_environment: JinjaEnvironment): + return jinja_environment.render_template_as_markup( + "screens/source_file_coverage/index.jinja", view_object=self ) - return template.render(view_object=self) def render_static_url(self, url: str): return self.link_renderer.render_static_url(url) diff --git a/strictdoc/export/html/generators/source_file_view_generator.py b/strictdoc/export/html/generators/source_file_view_generator.py index 934b9768f..de36ccde1 100644 --- a/strictdoc/export/html/generators/source_file_view_generator.py +++ b/strictdoc/export/html/generators/source_file_view_generator.py @@ -1,7 +1,7 @@ # mypy: disable-error-code="no-untyped-call,no-untyped-def,operator" -import html -from typing import List +from typing import List, Tuple +from markupsafe import Markup, escape from pygments import highlight from pygments.formatters.html import HtmlFormatter from pygments.lexers import get_lexer_for_filename @@ -46,8 +46,8 @@ 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[str] = [] - pygments_styles: str = "" + pygmented_source_file_lines: List[Markup] = [] + pygments_styles: Markup = Markup("") if len(source_file_lines) > 0: coverage_info: SourceFileTraceabilityInfo = ( @@ -89,7 +89,7 @@ def get_pygmented_source_lines( source_file: SourceFile, source_file_lines: List[str], coverage_info: SourceFileTraceabilityInfo, - ): + ) -> Tuple[List[Markup], Markup]: assert isinstance(source_file, SourceFile) assert isinstance(source_file_lines, list) assert isinstance(coverage_info, SourceFileTraceabilityInfo) @@ -202,12 +202,9 @@ def get_pygmented_source_lines( assert closing_bracket_index is not None after_line = source_line[closing_bracket_index:].rstrip() - before_line = html.escape(before_line) - after_line = html.escape(after_line) - pygmented_source_file_lines[pragma_line - 1] = ( - before_line, - after_line, + escape(before_line), + escape(after_line), pragma, ) pygments_styles = ( @@ -215,4 +212,6 @@ def get_pygmented_source_lines( + html_formatter.get_style_defs(".highlight") ) - return pygmented_source_file_lines, pygments_styles + return list(map(Markup, pygmented_source_file_lines)), Markup( + pygments_styles + ) diff --git a/strictdoc/export/html/generators/view_objects/diff_screen_results_view_object.py b/strictdoc/export/html/generators/view_objects/diff_screen_results_view_object.py index 938b02ddb..626bbff45 100644 --- a/strictdoc/export/html/generators/view_objects/diff_screen_results_view_object.py +++ b/strictdoc/export/html/generators/view_objects/diff_screen_results_view_object.py @@ -3,10 +3,9 @@ from datetime import datetime from typing import Optional -from jinja2 import Environment - from strictdoc import __version__ from strictdoc.core.project_config import ProjectConfig +from strictdoc.export.html.html_templates import JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer from strictdoc.git.change_generator import ChangeContainer @@ -59,8 +58,10 @@ def __init__( self.strictdoc_version = __version__ self.error_message: Optional[str] = None - def render_screen(self, jinja_environment: Environment): - template = jinja_environment.get_template("screens/git/index.jinja") + def render_screen(self, jinja_environment: JinjaEnvironment): + template = jinja_environment.environment.overlay( + autoescape=False + ).get_template("screens/git/index.jinja") return template.render(view_object=self) def render_url(self, url: str): diff --git a/strictdoc/export/html/generators/view_objects/diff_screen_view_object.py b/strictdoc/export/html/generators/view_objects/diff_screen_view_object.py index dd535906e..003139136 100644 --- a/strictdoc/export/html/generators/view_objects/diff_screen_view_object.py +++ b/strictdoc/export/html/generators/view_objects/diff_screen_view_object.py @@ -2,10 +2,9 @@ from dataclasses import dataclass from datetime import datetime -from jinja2 import Environment - from strictdoc import __version__ from strictdoc.core.project_config import ProjectConfig +from strictdoc.export.html.html_templates import JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer @@ -40,9 +39,10 @@ def __init__( self.is_running_on_server: bool = project_config.is_running_on_server self.strictdoc_version = __version__ - def render_screen(self, jinja_environment: Environment): - template = jinja_environment.get_template("screens/git/index.jinja") - return template.render(view_object=self) + def render_screen(self, jinja_environment: JinjaEnvironment): + return jinja_environment.render_template_as_markup( + "screens/git/index.jinja", view_object=self + ) def render_url(self, url: str): return self.link_renderer.render_url(url) diff --git a/strictdoc/export/html/generators/view_objects/document_screen_view_object.py b/strictdoc/export/html/generators/view_objects/document_screen_view_object.py index f1c54b507..2548f4917 100644 --- a/strictdoc/export/html/generators/view_objects/document_screen_view_object.py +++ b/strictdoc/export/html/generators/view_objects/document_screen_view_object.py @@ -3,7 +3,8 @@ from datetime import datetime from typing import List, Optional, Union -from jinja2 import Environment, Template +from jinja2 import Template +from markupsafe import Markup from strictdoc import __version__ from strictdoc.backend.sdoc.models.document import SDocDocument @@ -15,6 +16,7 @@ from strictdoc.core.project_config import ProjectConfig from strictdoc.core.traceability_index import TraceabilityIndex from strictdoc.export.html.document_type import DocumentType +from strictdoc.export.html.html_templates import JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer from strictdoc.server.helpers.turbo import render_turbo_stream @@ -63,68 +65,71 @@ def __init__( def has_included_document(self): return len(self.document.included_documents) > 0 - def render_screen(self, jinja_environment: Environment): + def render_screen(self, jinja_environment: JinjaEnvironment) -> Markup: if self.document_type.is_document: if self.document.config.layout == "Website": - template = jinja_environment.get_template( - "website/document/index.jinja" - ) - else: - template = jinja_environment.get_template( - "screens/document/document/index.jinja" + return jinja_environment.render_template_as_markup( + "website/document/index.jinja", view_object=self ) + return jinja_environment.render_template_as_markup( + "screens/document/document/index.jinja", view_object=self + ) elif self.document_type.is_table(): - template = jinja_environment.get_template( - "screens/document/table/index.jinja" + return jinja_environment.render_template_as_markup( + "screens/document/table/index.jinja", view_object=self ) elif self.document_type.is_trace(): - template = jinja_environment.get_template( - "screens/document/traceability/index.jinja" + return jinja_environment.render_template_as_markup( + "screens/document/traceability/index.jinja", view_object=self ) elif self.document_type.is_deeptrace: - template = jinja_environment.get_template( - "screens/document/traceability_deep/index.jinja" + return jinja_environment.render_template_as_markup( + "screens/document/traceability_deep/index.jinja", + view_object=self, ) elif self.document_type.is_pdf(): - template = jinja_environment.get_template( - "screens/document/pdf/index.jinja" + return jinja_environment.render_template_as_markup( + "screens/document/pdf/index.jinja", view_object=self ) else: raise NotImplementedError(self.document_type) - return template.render(view_object=self) - def render_table_screen(self, jinja_environment: Environment): - template = jinja_environment.get_template( - "screens/document/table/index.jinja" + def render_table_screen( + self, jinja_environment: JinjaEnvironment + ) -> Markup: + return jinja_environment.render_template_as_markup( + "screens/document/table/index.jinja", view_object=self ) - return template.render(view_object=self) - def render_trace_screen(self, jinja_environment: Environment): - template = jinja_environment.get_template( - "screens/document/traceability/index.jinja" + def render_trace_screen( + self, jinja_environment: JinjaEnvironment + ) -> Markup: + return jinja_environment.render_template_as_markup( + "screens/document/traceability/index.jinja", view_object=self ) - return template.render(view_object=self) - def render_updated_screen(self, jinja_environment: Environment) -> str: - template = jinja_environment.get_template( + def render_updated_screen( + self, jinja_environment: JinjaEnvironment + ) -> Markup: + output = jinja_environment.render_template_as_markup( "actions/" "document/" "create_requirement/" - "stream_created_requirement.jinja.html" + "stream_created_requirement.jinja.html", + view_object=self, ) - output = template.render(view_object=self) - toc_template = jinja_environment.get_template( - "actions/document/_shared/stream_updated_toc.jinja.html" + output += jinja_environment.render_template_as_markup( + "actions/document/_shared/stream_updated_toc.jinja.html", + view_object=self, ) - output += toc_template.render(view_object=self) return output def render_updated_nodes_and_toc( self, nodes: List[Union[SDocDocument, SDocNode]], - jinja_environment: Environment, + jinja_environment: JinjaEnvironment, ) -> str: output: str = "" @@ -139,20 +144,22 @@ def render_updated_nodes_and_toc( template_folder = "requirement" else: raise NotImplementedError - template = jinja_environment.get_template( - f"components/{template_folder}/index_extends_node.jinja" + content = jinja_environment.render_template_as_markup( + f"components/{template_folder}/index_extends_node.jinja", + view_object=self, + node=node_, ) output += render_turbo_stream( - content=template.render(view_object=self, node=node_), + content=content, action="replace", target=f"article-{node_.reserved_mid}", ) - toc_template = jinja_environment.get_template( - "screens/document/_shared/toc.jinja" + toc_content = jinja_environment.render_template_as_markup( + "screens/document/_shared/toc.jinja", view_object=self ) output += render_turbo_stream( - content=toc_template.render(view_object=self), + content=toc_content, action="update", target="frame-toc", ) @@ -160,24 +167,24 @@ def render_updated_nodes_and_toc( return output def render_update_document_content_with_moved_node( - self, jinja_environment: Environment, moved_node - ) -> str: - template = jinja_environment.get_template( - "screens/document/document/frame_document_content.jinja.html" + self, jinja_environment: JinjaEnvironment, moved_node + ) -> Markup: + content = jinja_environment.render_template_as_markup( + "screens/document/document/frame_document_content.jinja.html", + view_object=self, ) output = render_turbo_stream( - content=template.render(view_object=self), + content=content, action="replace", target="frame_document_content", ) - toc_template = jinja_environment.get_template( - "actions/document/_shared/stream_updated_toc.jinja.html" + toc_content = jinja_environment.render_template_as_markup( + "actions/document/_shared/stream_updated_toc.jinja.html", + view_object=self, + last_moved_node_id=moved_node.reserved_mid, ) output += render_turbo_stream( - toc_template.render( - view_object=self, - last_moved_node_id=moved_node.reserved_mid, - ), + toc_content, action="update", target="frame-toc", ) @@ -205,10 +212,10 @@ def folder_contains_including_documents(self, folder: Folder): return True return False - def render_url(self, url: str): - return self.link_renderer.render_url(url) + def render_url(self, url: str) -> Markup: + return Markup(self.link_renderer.render_url(url)) - def render_node_link(self, node, context_document, document_type): + def render_node_link(self, node, context_document, document_type) -> str: assert node is not None, node return self.link_renderer.render_node_link( node, context_document, document_type @@ -219,7 +226,7 @@ def render_document_link( document: SDocDocument, context_document: SDocDocument, document_type_string: str, - ): + ) -> str: assert document is not None, document return self.link_renderer.render_node_link( document, context_document, DocumentType(document_type_string) @@ -233,41 +240,43 @@ def render_standalone_document_link( raise NotImplementedError root_prefix = document.meta.get_root_path_prefix() document_link = document.meta.get_html_standalone_document_link() - if len(root_prefix) == 0: - return document_link - return "/".join((root_prefix, document_link)) + return ( + document_link + if len(root_prefix) == 0 + else "/".join((root_prefix, document_link)) + ) - def render_static_url(self, url: str): - return self.link_renderer.render_static_url(url) + def render_static_url(self, url: str) -> str: + return Markup(self.link_renderer.render_static_url(url)) - def render_local_anchor(self, node): + def render_local_anchor(self, node) -> str: return self.link_renderer.render_local_anchor(node) - def render_node_statement(self, node): + def render_node_statement(self, node) -> Markup: return self.markup_renderer.render_node_statement( self.document_type, node ) - def render_truncated_node_statement(self, node): + def render_truncated_node_statement(self, node) -> Markup: return self.markup_renderer.render_truncated_node_statement( self.document_type, node ) - def render_node_rationale(self, node): + def render_node_rationale(self, node) -> Markup: return self.markup_renderer.render_node_rationale( self.document_type, node ) - def render_node_field(self, node_field: SDocNodeField): + def render_node_field(self, node_field: SDocNodeField) -> Markup: assert isinstance(node_field, SDocNodeField), node_field return self.markup_renderer.render_node_field( self.document_type, node_field ) - def get_page_title(self): + def get_page_title(self) -> str: return self.document_type.get_page_title() - def date_today(self): + def date_today(self) -> str: return datetime.today().strftime("%Y-%m-%d") def get_document_by_path(self, full_path: str) -> SDocDocument: diff --git a/strictdoc/export/html/generators/view_objects/nestor_view_object.py b/strictdoc/export/html/generators/view_objects/nestor_view_object.py index 00b1cb642..cc62f01f6 100644 --- a/strictdoc/export/html/generators/view_objects/nestor_view_object.py +++ b/strictdoc/export/html/generators/view_objects/nestor_view_object.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from typing import Optional -from jinja2 import Environment - from strictdoc import __version__ from strictdoc.backend.sdoc.models.document import SDocDocument from strictdoc.backend.sdoc.models.document_view import DocumentView @@ -10,7 +8,7 @@ from strictdoc.core.project_config import ProjectConfig from strictdoc.core.traceability_index import TraceabilityIndex from strictdoc.export.html.document_type import DocumentType -from strictdoc.export.html.html_templates import HTMLTemplates +from strictdoc.export.html.html_templates import HTMLTemplates, JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer @@ -55,9 +53,10 @@ def __init__( self.link_document_type: DocumentType = DocumentType.document() self.document: Optional[SDocDocument] = None - def render_screen(self, jinja_environment: Environment) -> str: - template = jinja_environment.get_template("screens/nestor/index.jinja") - return template.render(view_object=self) + def render_screen(self, jinja_environment: JinjaEnvironment) -> str: + return jinja_environment.render_template_as_markup( + "screens/nestor/index.jinja", view_object=self + ) def render_static_url(self, url: str) -> str: return self.link_renderer.render_static_url(url) diff --git a/strictdoc/export/html/generators/view_objects/project_statistics_view_object.py b/strictdoc/export/html/generators/view_objects/project_statistics_view_object.py index 84dd90cfe..e2306eb83 100644 --- a/strictdoc/export/html/generators/view_objects/project_statistics_view_object.py +++ b/strictdoc/export/html/generators/view_objects/project_statistics_view_object.py @@ -2,8 +2,6 @@ from dataclasses import dataclass from datetime import datetime -from jinja2 import Environment - from strictdoc import __version__ from strictdoc.core.document_tree_iterator import DocumentTreeIterator from strictdoc.core.project_config import ProjectConfig @@ -11,6 +9,7 @@ from strictdoc.export.html.generators.view_objects.project_tree_stats import ( DocumentTreeStats, ) +from strictdoc.export.html.html_templates import JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer @@ -35,11 +34,10 @@ def __init__( self.is_running_on_server: bool = project_config.is_running_on_server self.strictdoc_version = __version__ - def render_screen(self, jinja_environment: Environment): - template = jinja_environment.get_template( - "screens/project_statistics/index.jinja" + def render_screen(self, jinja_environment: JinjaEnvironment): + return jinja_environment.render_template_as_markup( + "screens/project_statistics/index.jinja", view_object=self ) - return template.render(view_object=self) def render_static_url(self, url: str): return self.link_renderer.render_static_url(url) diff --git a/strictdoc/export/html/generators/view_objects/project_tree_view_object.py b/strictdoc/export/html/generators/view_objects/project_tree_view_object.py index 0678f750b..4f2559ef1 100644 --- a/strictdoc/export/html/generators/view_objects/project_tree_view_object.py +++ b/strictdoc/export/html/generators/view_objects/project_tree_view_object.py @@ -1,12 +1,11 @@ # mypy: disable-error-code="no-any-return,no-untyped-call,no-untyped-def" from dataclasses import dataclass -from jinja2 import Environment - from strictdoc import __version__ from strictdoc.core.document_tree_iterator import DocumentTreeIterator from strictdoc.core.project_config import ProjectConfig from strictdoc.core.traceability_index import TraceabilityIndex +from strictdoc.export.html.html_templates import JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer @@ -36,11 +35,10 @@ def __init__( traceability_index.contains_included_documents ) - def render_screen(self, jinja_environment: Environment): - template = jinja_environment.get_template( - "screens/project_index/index.jinja" + def render_screen(self, jinja_environment: JinjaEnvironment): + return jinja_environment.render_template_as_markup( + "screens/project_index/index.jinja", view_object=self ) - return template.render(view_object=self) def render_static_url(self, url: str): return self.link_renderer.render_static_url(url) diff --git a/strictdoc/export/html/generators/view_objects/search_screen_view_object.py b/strictdoc/export/html/generators/view_objects/search_screen_view_object.py index b64fd9845..ff61ac21b 100644 --- a/strictdoc/export/html/generators/view_objects/search_screen_view_object.py +++ b/strictdoc/export/html/generators/view_objects/search_screen_view_object.py @@ -3,16 +3,15 @@ from datetime import datetime from typing import Optional -from jinja2 import Environment - from strictdoc import __version__ +from strictdoc.backend.sdoc.models.any_node import SDocAnyNode from strictdoc.backend.sdoc.models.document import SDocDocument from strictdoc.backend.sdoc.models.document_view import DocumentView from strictdoc.core.document_tree_iterator import DocumentTreeIterator from strictdoc.core.project_config import ProjectConfig from strictdoc.core.traceability_index import TraceabilityIndex from strictdoc.export.html.document_type import DocumentType -from strictdoc.export.html.html_templates import HTMLTemplates +from strictdoc.export.html.html_templates import HTMLTemplates, JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer @@ -25,8 +24,8 @@ def __init__( traceability_index: TraceabilityIndex, project_config: ProjectConfig, templates: HTMLTemplates, - search_results, - search_value, + search_results: SDocAnyNode, + search_value: str, error, ): self.traceability_index: TraceabilityIndex = traceability_index @@ -66,9 +65,10 @@ def render_truncated_node_statement(self, node): self.document_type, node ) - def render_screen(self, jinja_environment: Environment): - template = jinja_environment.get_template("screens/search/index.jinja") - return template.render(view_object=self) + def render_screen(self, jinja_environment: JinjaEnvironment): + return jinja_environment.render_template_as_markup( + "screens/search/index.jinja", view_object=self + ) def is_empty_tree(self) -> bool: return self.document_tree_iterator.is_empty_tree() 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 25442fd93..9ff0dbbb8 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 @@ -3,13 +3,14 @@ from datetime import datetime from typing import List -from jinja2 import Environment +from markupsafe import Markup from strictdoc import __version__ from strictdoc.core.document_tree_iterator import DocumentTreeIterator from strictdoc.core.finders.source_files_finder import SourceFile from strictdoc.core.project_config import ProjectConfig from strictdoc.core.traceability_index import TraceabilityIndex +from strictdoc.export.html.html_templates import JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer @@ -24,16 +25,16 @@ def __init__( link_renderer: LinkRenderer, markup_renderer: MarkupRenderer, source_file: SourceFile, - pygments_styles: str, - pygmented_source_file_lines: List[str], + pygments_styles: Markup, + pygmented_source_file_lines: List[Markup], ): self.traceability_index: TraceabilityIndex = traceability_index self.project_config: ProjectConfig = project_config self.link_renderer: LinkRenderer = link_renderer self.markup_renderer: MarkupRenderer = markup_renderer self.source_file: SourceFile = source_file - self.pygments_styles: str = pygments_styles - self.pygmented_source_file_lines: List[str] = ( + self.pygments_styles: Markup = pygments_styles + self.pygmented_source_file_lines: List[Markup] = ( pygmented_source_file_lines ) @@ -44,11 +45,10 @@ def __init__( self.is_running_on_server: bool = project_config.is_running_on_server self.strictdoc_version = __version__ - def render_screen(self, jinja_environment: Environment): - template = jinja_environment.get_template( - "screens/source_file_view/index.jinja" + def render_screen(self, jinja_environment: JinjaEnvironment) -> Markup: + return jinja_environment.render_template_as_markup( + "screens/source_file_view/index.jinja", view_object=self ) - return template.render(view_object=self) def is_empty_tree(self) -> bool: return self.document_tree_iterator.is_empty_tree() diff --git a/strictdoc/export/html/generators/view_objects/traceability_matrix_view_object.py b/strictdoc/export/html/generators/view_objects/traceability_matrix_view_object.py index 3d63e213e..fa886af54 100644 --- a/strictdoc/export/html/generators/view_objects/traceability_matrix_view_object.py +++ b/strictdoc/export/html/generators/view_objects/traceability_matrix_view_object.py @@ -1,15 +1,13 @@ # mypy: disable-error-code="arg-type,no-any-return,no-untyped-call,no-untyped-def,union-attr,type-arg" from typing import Dict, List, Optional, Tuple -from jinja2 import Environment - from strictdoc import __version__ from strictdoc.backend.sdoc.models.document import SDocDocument from strictdoc.backend.sdoc.models.document_grammar import DocumentGrammar from strictdoc.core.document_tree_iterator import DocumentTreeIterator from strictdoc.core.project_config import ProjectConfig from strictdoc.core.traceability_index import TraceabilityIndex -from strictdoc.export.html.html_templates import HTMLTemplates +from strictdoc.export.html.html_templates import HTMLTemplates, JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer @@ -42,11 +40,10 @@ def iterate_documents(self): self.traceability_index.document_tree.document_list, ) - def render_screen(self, jinja_environment: Environment): - template = jinja_environment.get_template( - "screens/traceability_matrix/index.jinja" + def render_screen(self, jinja_environment: JinjaEnvironment): + return jinja_environment.render_template_as_markup( + "screens/traceability_matrix/index.jinja", view_object=self ) - return template.render(view_object=self) def render_static_url(self, url: str): return self.link_renderer.render_static_url(url) diff --git a/strictdoc/export/html/html_templates.py b/strictdoc/export/html/html_templates.py index 04f1e11be..154e70f12 100644 --- a/strictdoc/export/html/html_templates.py +++ b/strictdoc/export/html/html_templates.py @@ -13,10 +13,12 @@ FileSystemLoader, ModuleLoader, StrictUndefined, + Template, TemplateRuntimeError, nodes, ) from jinja2.ext import Extension +from markupsafe import Markup from strictdoc import environment from strictdoc.core.project_config import ProjectConfig @@ -24,6 +26,23 @@ from strictdoc.helpers.timing import measure_performance +class JinjaEnvironment: + environment: Environment + + def __init__(self, environment: Environment): + self.environment = environment + + def get_template(self, *args, **kwargs) -> Template: + return self.environment.get_template(*args, **kwargs) + + def render_template_as_markup( + self, template: str, *args, **kwargs + ) -> Markup: + return Markup( + self.environment.get_template(template).render(*args, **kwargs) + ) + + # https://stackoverflow.com/questions/21778252/how-to-raise-an-exception-in-a-jinja2-macro class AssertExtension(Extension): # This is our keyword(s): @@ -90,7 +109,7 @@ def create( return NormalHTMLTemplates() - def jinja_environment(self) -> Environment: + def jinja_environment(self) -> JinjaEnvironment: raise NotImplementedError def reset_jinja_environment_if_outdated( @@ -101,13 +120,18 @@ def reset_jinja_environment_if_outdated( class NormalHTMLTemplates(HTMLTemplates): def __init__(self): - self._jinja_environment: Environment = Environment( - loader=FileSystemLoader(environment.get_path_to_html_templates()), - undefined=StrictUndefined, - extensions=[AssertExtension], + self._jinja_environment: JinjaEnvironment = JinjaEnvironment( + Environment( + loader=FileSystemLoader( + environment.get_path_to_html_templates() + ), + undefined=StrictUndefined, + extensions=[AssertExtension], + autoescape=True, + ) ) - def jinja_environment(self) -> Environment: + def jinja_environment(self) -> JinjaEnvironment: return self._jinja_environment def reset_jinja_environment_if_outdated( @@ -130,7 +154,7 @@ def __init__(self, project_config: ProjectConfig): CompiledHTMLTemplates.PATH_TO_JINJA_CACHE_DIR, path_to_output_dir_hash, ) - self._jinja_environment: Optional[Environment] = None + self._jinja_environment: Optional[JinjaEnvironment] = None def compile_jinja_templates(self): if os.path.isdir(self.path_to_jinja_cache_bucket_dir): @@ -139,6 +163,7 @@ def compile_jinja_templates(self): loader=FileSystemLoader(environment.get_path_to_html_templates()), undefined=StrictUndefined, extensions=[AssertExtension], + autoescape=True, ) # TODO: Check if this line is still needed (might be some older workaround). jinja_environment.globals.update(isinstance=isinstance) @@ -162,14 +187,17 @@ def filter_function_(name: str) -> bool: ignore_errors=False, ) - def jinja_environment(self) -> Environment: + def jinja_environment(self) -> JinjaEnvironment: if self._jinja_environment is not None: return self._jinja_environment assert os.path.isdir(self.path_to_jinja_cache_bucket_dir) - self._jinja_environment = Environment( - loader=ModuleLoader(self.path_to_jinja_cache_bucket_dir), - undefined=StrictUndefined, - extensions=[AssertExtension], + self._jinja_environment = JinjaEnvironment( + Environment( + loader=ModuleLoader(self.path_to_jinja_cache_bucket_dir), + undefined=StrictUndefined, + extensions=[AssertExtension], + autoescape=True, + ) ) return self._jinja_environment diff --git a/strictdoc/export/html/renderers/html_fragment_writer.py b/strictdoc/export/html/renderers/html_fragment_writer.py index fdb44eeda..82397ea0b 100644 --- a/strictdoc/export/html/renderers/html_fragment_writer.py +++ b/strictdoc/export/html/renderers/html_fragment_writer.py @@ -1,7 +1,10 @@ +from markupsafe import Markup + + class HTMLFragmentWriter: @staticmethod - def write(text_fragment: str) -> str: - return text_fragment + def write(text_fragment: str) -> Markup: + return Markup(text_fragment) @staticmethod def write_anchor_link(title: str, href: str) -> str: diff --git a/strictdoc/export/html/renderers/markup_renderer.py b/strictdoc/export/html/renderers/markup_renderer.py index dd2c99f44..83a642f2d 100644 --- a/strictdoc/export/html/renderers/markup_renderer.py +++ b/strictdoc/export/html/renderers/markup_renderer.py @@ -1,5 +1,7 @@ from typing import Dict, Optional, Tuple, Union +from markupsafe import Markup + from strictdoc.backend.sdoc.models.anchor import Anchor from strictdoc.backend.sdoc.models.document import SDocDocument from strictdoc.backend.sdoc.models.inline_link import InlineLink @@ -84,7 +86,7 @@ def __init__( # FIXME: Now that the underlying RST fragment caching is in place, # This caching could be removed. It is unlikely that it adds any serious # performance improvement. - self.cache: Dict[Tuple[DocumentType, SDocNodeField], str] = {} + self.cache: Dict[Tuple[DocumentType, SDocNodeField], Markup] = {} self.template_anchor = html_templates.jinja_environment().get_template( "rst/anchor.jinja" @@ -92,13 +94,13 @@ def __init__( def render_node_statement( self, document_type: DocumentType, node: SDocNode - ) -> str: + ) -> Markup: assert isinstance(node, SDocNode) return self.render_node_field(document_type, node.get_content_field()) def render_truncated_node_statement( self, document_type: DocumentType, node: SDocNode - ) -> str: + ) -> Markup: assert isinstance(node, SDocNode) # FIXME: Double-check and switch to truncating using CSS. # https://github.com/strictdoc-project/strictdoc/issues/1925 @@ -106,7 +108,7 @@ def render_truncated_node_statement( def render_node_rationale( self, document_type: DocumentType, node: SDocNode - ) -> str: + ) -> Markup: assert isinstance(node, SDocNode) return self.render_node_field( document_type, @@ -115,7 +117,7 @@ def render_node_rationale( def render_node_field( self, document_type: DocumentType, node_field: SDocNodeField - ) -> str: + ) -> Markup: assert isinstance(node_field, SDocNodeField), node_field if (document_type, node_field) in self.cache: @@ -149,6 +151,7 @@ def render_node_field( else: raise NotImplementedError prev_part = part + output = self.fragment_writer.write(parts_output) self.cache[(document_type, node_field)] = output diff --git a/strictdoc/export/html/renderers/text_to_html_writer.py b/strictdoc/export/html/renderers/text_to_html_writer.py index 12d499527..bb1bbfa27 100644 --- a/strictdoc/export/html/renderers/text_to_html_writer.py +++ b/strictdoc/export/html/renderers/text_to_html_writer.py @@ -1,10 +1,10 @@ -import html +from markupsafe import Markup, escape class TextToHtmlWriter: @staticmethod - def write(text_fragment: str) -> str: - return html.escape(text_fragment, quote=True).replace("\n", "
\n") + def write(text_fragment: str) -> Markup: + return escape(text_fragment).replace("\n", Markup("
\n")) @staticmethod def write_anchor_link(title: str, href: str) -> str: diff --git a/strictdoc/export/html/templates/components/form/row/row_uid_with_reset/example.jinja.html b/strictdoc/export/html/templates/components/form/row/row_uid_with_reset/example.jinja.html index bb960d7b7..e7d9cd07a 100644 --- a/strictdoc/export/html/templates/components/form/row/row_uid_with_reset/example.jinja.html +++ b/strictdoc/export/html/templates/components/form/row/row_uid_with_reset/example.jinja.html @@ -7,7 +7,7 @@ {% set text_field_row_context.field = field_ %} {% set text_field_row_context.field_type = "singleline" %} {% set text_field_row_context.reference_mid = form_object.requirement_mid %} - {%- if field_.field_name == "UID" and field_.field_escaped_value == "" -%} + {%- if field_.field_name == "UID" and field_.field_value == "" -%} {# this template is turbo-frame and has a button to reset to the default value: #} {% include "components/form/row/row_uid_with_reset/frame.jinja" %} diff --git a/strictdoc/export/html/templates/components/form/row/row_uid_with_reset/frame.jinja b/strictdoc/export/html/templates/components/form/row/row_uid_with_reset/frame.jinja index 7cc1cf04f..928b30430 100644 --- a/strictdoc/export/html/templates/components/form/row/row_uid_with_reset/frame.jinja +++ b/strictdoc/export/html/templates/components/form/row/row_uid_with_reset/frame.jinja @@ -41,7 +41,7 @@ field_label = text_field_row_context.field.field_name, field_placeholder = "Enter "~placeholder_name~" here...", field_type = text_field_row_context.field_type, - field_value = text_field_row_context.field.field_escaped_value, + field_value = text_field_row_context.field.field_value, testid_postfix = text_field_row_context.field.field_name %} {%- include "components/form/field/contenteditable/index.jinja" %} diff --git a/strictdoc/export/html/templates/components/form/row/row_with_comment.jinja b/strictdoc/export/html/templates/components/form/row/row_with_comment.jinja index 7790493a7..b75cc05b6 100644 --- a/strictdoc/export/html/templates/components/form/row/row_with_comment.jinja +++ b/strictdoc/export/html/templates/components/form/row/row_with_comment.jinja @@ -36,7 +36,7 @@ field_label = comment_field_row_context.field.field_name, field_placeholder = "Enter comment here...", field_type = "multiline", - field_value = comment_field_row_context.field.field_escaped_value, + field_value = comment_field_row_context.field.field_value, mid = comment_field_row_context.field.field_mid, testid_postfix = "COMMENT" %} diff --git a/strictdoc/export/html/templates/components/form/row/row_with_text_field.jinja b/strictdoc/export/html/templates/components/form/row/row_with_text_field.jinja index 039dfe5f9..d145cadd6 100644 --- a/strictdoc/export/html/templates/components/form/row/row_with_text_field.jinja +++ b/strictdoc/export/html/templates/components/form/row/row_with_text_field.jinja @@ -39,7 +39,7 @@ field_label = text_field_row_context.field.field_name, field_placeholder = "Enter "~placeholder_name~" here...", field_type = text_field_row_context.field_type, - field_value = text_field_row_context.field.field_escaped_value, + field_value = text_field_row_context.field.field_value, testid_postfix = text_field_row_context.field.field_name %} {%- include "components/form/field/contenteditable/index.jinja" %} diff --git a/strictdoc/export/html/templates/components/node_field/section_h/index.jinja b/strictdoc/export/html/templates/components/node_field/section_h/index.jinja index f2a3445a6..ab81f3988 100644 --- a/strictdoc/export/html/templates/components/node_field/section_h/index.jinja +++ b/strictdoc/export/html/templates/components/node_field/section_h/index.jinja @@ -14,7 +14,7 @@ {%- if sdoc_entity.context.title_number_string -%} {#- add title 'number' part to the accumulator -#} - {%- set field_content_ = field_content_ + sdoc_entity.context.title_number_string + ". " -%} + {%- set field_content_ = field_content_ + sdoc_entity.context.title_number_string + ". "|safe -%} {%- endif -%} {%- set title = sdoc_entity.reserved_title if sdoc_entity.is_requirement else sdoc_entity.title -%} diff --git a/strictdoc/export/html/templates/components/node_field/section_title/index.jinja b/strictdoc/export/html/templates/components/node_field/section_title/index.jinja index 6c6ba8804..4b5a2cc42 100644 --- a/strictdoc/export/html/templates/components/node_field/section_title/index.jinja +++ b/strictdoc/export/html/templates/components/node_field/section_title/index.jinja @@ -9,7 +9,7 @@ {%- if sdoc_entity.context.title_number_string -%} {#- add title 'number' part to the accumulator -#} - {%- set field_content_ = field_content_ + sdoc_entity.context.title_number_string + ". " -%} + {%- set field_content_ = field_content_ + sdoc_entity.context.title_number_string + ". "|safe -%} {%- endif -%} {%- set title = sdoc_entity.reserved_title if sdoc_entity.is_requirement else sdoc_entity.title -%} diff --git a/strictdoc/export/html/templates/components/node_field/title/index.jinja b/strictdoc/export/html/templates/components/node_field/title/index.jinja index 5f115c90e..77934acad 100644 --- a/strictdoc/export/html/templates/components/node_field/title/index.jinja +++ b/strictdoc/export/html/templates/components/node_field/title/index.jinja @@ -8,7 +8,7 @@ {%- if title_number is true -%} {%- if sdoc_entity.context.title_number_string %} {#- add title 'number' part to the accumulator -#} - {%- set field_content_ = field_content_ + sdoc_entity.context.title_number_string + ". " -%} + {%- set field_content_ = field_content_ + sdoc_entity.context.title_number_string + ". "|safe -%} {%- endif -%} {%- endif -%} diff --git a/strictdoc/export/html/templates/screens/document/document/frame_requirement_form.jinja b/strictdoc/export/html/templates/screens/document/document/frame_requirement_form.jinja index fec5903e2..62145cd50 100644 --- a/strictdoc/export/html/templates/screens/document/document/frame_requirement_form.jinja +++ b/strictdoc/export/html/templates/screens/document/document/frame_requirement_form.jinja @@ -35,7 +35,7 @@ {% set text_field_row_context.field_editable = true %} {% set text_field_row_context.field_type = "singleline" %} {% set text_field_row_context.reference_mid = form_object.requirement_mid %} - {%- if form_object.element_type != "TEXT" and field_.field_name == "UID" and field_.field_escaped_value == "" -%} + {%- if form_object.element_type != "TEXT" and field_.field_name == "UID" and field_.field_value == "" -%} {# this template is turbo-frame and has a button to reset to the default value: #} {% include "components/form/row/row_uid_with_reset/frame.jinja" %} @@ -95,7 +95,7 @@ {# If comments have not yet been added, show only the add field button below, and do not display the code of the field itself: #} - {%- if field_.field_escaped_value|length > 0 -%} + {%- if field_.field_value|length > 0 -%} {% set comment_field_row_context.field = field_ %} {% set comment_field_row_context.field_editable = true %} {% set comment_field_row_context.errors = form_object.get_errors(field_.field_name) %} diff --git a/strictdoc/export/html/templates/screens/document/document/frame_section_form.jinja b/strictdoc/export/html/templates/screens/document/document/frame_section_form.jinja index 9292a3e7c..bd7f26595 100644 --- a/strictdoc/export/html/templates/screens/document/document/frame_section_form.jinja +++ b/strictdoc/export/html/templates/screens/document/document/frame_section_form.jinja @@ -37,7 +37,7 @@ {% set text_field_row_context.field_editable = true %} {% set text_field_row_context.field_type = "singleline" %} {% set text_field_row_context.reference_mid = form_object.section_mid %} - {%- if not is_new_section and form_object.section_uid_field.field_escaped_value == "" -%} + {%- if not is_new_section and form_object.section_uid_field.field_value == "" -%} {# this template is turbo-frame and has a button to reset to the default value: #} {% include "components/form/row/row_uid_with_reset/frame.jinja" %} diff --git a/strictdoc/export/html/templates/screens/git/fields/requirement_fields.jinja b/strictdoc/export/html/templates/screens/git/fields/requirement_fields.jinja index 3e234ee8e..0f11508d5 100644 --- a/strictdoc/export/html/templates/screens/git/fields/requirement_fields.jinja +++ b/strictdoc/export/html/templates/screens/git/fields/requirement_fields.jinja @@ -13,7 +13,7 @@ {% endif %}
- {%- for requirement_field_triple_ in requirement.enumerate_all_fields_escaped() -%} + {%- for requirement_field_triple_ in requirement.enumerate_all_fields() -%} {%- set is_multiline = requirement_field_triple_[0].is_multiline() -%} {% if requirement_change is not none -%} diff --git a/strictdoc/export/html/templates/screens/source_file_view/index.jinja b/strictdoc/export/html/templates/screens/source_file_view/index.jinja index 60be0b397..5af33aa93 100644 --- a/strictdoc/export/html/templates/screens/source_file_view/index.jinja +++ b/strictdoc/export/html/templates/screens/source_file_view/index.jinja @@ -22,7 +22,7 @@ {{ super() }} {% endblock head %} diff --git a/strictdoc/export/rst/rst_to_html_fragment_writer.py b/strictdoc/export/rst/rst_to_html_fragment_writer.py index af2101a7a..c62a07c81 100644 --- a/strictdoc/export/rst/rst_to_html_fragment_writer.py +++ b/strictdoc/export/rst/rst_to_html_fragment_writer.py @@ -11,6 +11,7 @@ from docutils.core import publish_parts from docutils.parsers.rst import directives, roles from docutils.utils import SystemMessage +from markupsafe import Markup from strictdoc.backend.sdoc.models.document import SDocDocument from strictdoc.export.rst.directives.raw_html_role import raw_html_role @@ -62,12 +63,12 @@ def __init__( self.source_path: str = "" self.context_document: Optional[SDocDocument] = context_document - def write(self, rst_fragment: str) -> str: + def write(self, rst_fragment: str) -> Markup: assert isinstance(rst_fragment, str), rst_fragment # FIXME: This is broken. if len(rst_fragment) < 0: - return self._write_no_cache(rst_fragment) + return Markup(self._write_no_cache(rst_fragment)) path_to_rst_fragment_bucket_dir = os.path.join( self.path_to_rst_cache_dir, str(len(rst_fragment)) @@ -81,7 +82,7 @@ def write(self, rst_fragment: str) -> str: with open( path_to_cached_fragment, "rb" ) as cached_fragment_file_: - return cached_fragment_file_.read().decode("UTF-8") + return Markup(cached_fragment_file_.read().decode("UTF-8")) else: Path(path_to_rst_fragment_bucket_dir).mkdir( parents=True, exist_ok=True @@ -93,7 +94,7 @@ def write(self, rst_fragment: str) -> str: with open(path_to_cached_fragment, "wb") as cached_fragment_file_: cached_fragment_file_.write(rendered_html_bytes) - return rendered_html + return Markup(rendered_html) def _write_no_cache(self, rst_fragment: str) -> str: assert isinstance(rst_fragment, str), rst_fragment @@ -187,7 +188,7 @@ def write_with_validation(self, rst_fragment): return html, None @staticmethod - def write_anchor_link(title, href): + def write_anchor_link(title, href) -> str: return f"""\ :rawhtml:`🔗 {title}`\ """ diff --git a/strictdoc/server/helpers/turbo.py b/strictdoc/server/helpers/turbo.py index 47bed28af..9b468c21a 100644 --- a/strictdoc/server/helpers/turbo.py +++ b/strictdoc/server/helpers/turbo.py @@ -1,3 +1,6 @@ +from markupsafe import Markup + + # mypy: disable-error-code="no-untyped-def" def render_turbo_stream(content: str, action: str, target: str): assert action in ("append", "replace", "update") @@ -9,4 +12,4 @@ def render_turbo_stream(content: str, action: str, target: str): """ - return turbo_stream + return Markup(turbo_stream) diff --git a/strictdoc/server/routers/main_router.py b/strictdoc/server/routers/main_router.py index 54c72f8bb..d0523cead 100644 --- a/strictdoc/server/routers/main_router.py +++ b/strictdoc/server/routers/main_router.py @@ -1,6 +1,5 @@ # mypy: disable-error-code="arg-type,attr-defined,no-any-return,no-redef,no-untyped-call,no-untyped-def,union-attr" import copy -import html import os import re from mimetypes import guess_type @@ -8,7 +7,6 @@ from typing import Dict, List, Optional, Union from fastapi import APIRouter, Form, UploadFile -from jinja2 import Environment from reqif.models.error_handling import ReqIFXMLParsingError from reqif.parser import ReqIFParser from reqif.unparser import ReqIFUnparser @@ -113,7 +111,7 @@ SearchScreenViewObject, ) from strictdoc.export.html.html_generator import HTMLGenerator -from strictdoc.export.html.html_templates import HTMLTemplates +from strictdoc.export.html.html_templates import HTMLTemplates, JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer from strictdoc.export.html2pdf.pdf_print_driver import PDFPrintDriver @@ -184,7 +182,7 @@ def create_main_router( export_output_html_root=project_config.export_output_html_root, ) - def env() -> Environment: + def env() -> JinjaEnvironment: return html_templates.jinja_environment() router = APIRouter() @@ -202,12 +200,6 @@ def requirement__show_full(reference_mid: str): requirement: SDocNode = ( export_action.traceability_index.get_node_by_mid(MID(reference_mid)) ) - template = env().get_template( - "actions/" - "node/" - "show_full_node/" - "stream_show_full_requirement.jinja" - ) link_renderer = LinkRenderer( root_path=requirement.document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -229,8 +221,13 @@ def requirement__show_full(reference_mid: str): markup_renderer=markup_renderer, standalone=False, ) - output = template.render( - view_object=view_object, requirement=requirement + output = env().render_template_as_markup( + "actions/" + "node/" + "show_full_node/" + "stream_show_full_requirement.jinja", + view_object=view_object, + requirement=requirement, ) return HTMLResponse( content=output, @@ -245,12 +242,6 @@ def section__show_full(reference_mid: str): section: SDocSection = export_action.traceability_index.get_node_by_mid( MID(reference_mid) ) - template = env().get_template( - "actions/" - "node/" - "show_full_node/" - "stream_show_full_section.jinja" - ) link_renderer = LinkRenderer( root_path=section.document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -266,7 +257,11 @@ def section__show_full(reference_mid: str): current_view: ViewElement = section.document.view.get_current_view( project_config.view ) - output = template.render( + output = env().render_template_as_markup( + "actions/" + "node/" + "show_full_node/" + "stream_show_full_section.jinja", renderer=markup_renderer, section=section, traceability_index=export_action.traceability_index, @@ -318,9 +313,6 @@ def get_new_section( else: raise NotImplementedError - template = env().get_template( - "actions/document/create_section/stream_new_section.jinja.html" - ) link_renderer = LinkRenderer( root_path=document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -333,7 +325,8 @@ def get_new_section( config=project_config, context_document=document, ) - output = template.render( + output = env().render_template_as_markup( + "actions/document/create_section/stream_new_section.jinja.html", renderer=markup_renderer, form_object=section_form_object, reference_mid=reference_mid, @@ -393,9 +386,6 @@ async def create_section(request: Request): for error_key, errors in validation_error.errors.items(): for error in errors: form_object.add_error(error_key, error) - template = env().get_template( - "actions/document/create_section/stream_new_section.jinja.html" - ) link_renderer = LinkRenderer( root_path=context_document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -408,7 +398,8 @@ async def create_section(request: Request): config=project_config, context_document=context_document, ) - output = template.render( + output = env().render_template_as_markup( + "actions/document/create_section/stream_new_section.jinja.html", renderer=markup_renderer, form_object=form_object, reference_mid=reference_mid, @@ -459,17 +450,13 @@ async def create_section(request: Request): ) # Rendering back the Turbo template. - template = env().get_template( - "actions/document/create_section/stream_created_section.jinja.html" - ) - output = template.render( + output = env().render_template_as_markup( + "actions/document/create_section/stream_created_section.jinja.html", view_object=view_object, ) - toc_template = env().get_template( - "actions/document/_shared/stream_updated_toc.jinja.html" - ) - output += toc_template.render( + output += env().render_template_as_markup( + "actions/document/_shared/stream_updated_toc.jinja.html", view_object=view_object, ) return HTMLResponse( @@ -488,9 +475,6 @@ def get_edit_section(node_id: str, context_document_mid: str): form_object = SectionFormObject.create_from_section( section=section, context_document_mid=context_document_mid ) - template = env().get_template( - "actions/document/edit_section/stream_edit_section.jinja.html" - ) link_renderer = LinkRenderer( root_path=section.document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -503,7 +487,8 @@ def get_edit_section(node_id: str, context_document_mid: str): config=project_config, context_document=section.document, ) - output = template.render( + output = env().render_template_as_markup( + "actions/document/edit_section/stream_edit_section.jinja.html", renderer=markup_renderer, form_object=form_object, document_type=DocumentType.document(), @@ -549,9 +534,6 @@ async def put_update_section(request: Request): for error_key, errors in validation_error.errors.items(): for error in errors: form_object.add_error(error_key, error) - template = env().get_template( - "actions/document/edit_section/stream_edit_section.jinja.html" - ) link_renderer = LinkRenderer( root_path=section.document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -564,7 +546,8 @@ async def put_update_section(request: Request): config=project_config, context_document=section.document, ) - output = template.render( + output = env().render_template_as_markup( + "actions/document/edit_section/stream_edit_section.jinja.html", renderer=markup_renderer, link_renderer=link_renderer, form_object=form_object, @@ -599,9 +582,6 @@ async def put_update_section(request: Request): ) # Rendering back the Turbo template. - template = env().get_template( - "actions/document/edit_section/stream_updated_section.jinja.html" - ) link_renderer = LinkRenderer( root_path=section.document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -623,11 +603,15 @@ async def put_update_section(request: Request): markup_renderer=markup_renderer, standalone=False, ) - output = template.render(node=section, view_object=view_object) - toc_template = env().get_template( - "actions/document/_shared/stream_updated_toc.jinja.html" + output = env().render_template_as_markup( + "actions/document/edit_section/stream_updated_section.jinja.html", + node=section, + view_object=view_object, + ) + output += env().render_template_as_markup( + "actions/document/_shared/stream_updated_toc.jinja.html", + view_object=view_object, ) - output += toc_template.render(view_object=view_object) return HTMLResponse( content=output, headers={ @@ -637,13 +621,13 @@ async def put_update_section(request: Request): @router.get("/actions/document/cancel_new_section", response_class=Response) def cancel_new_section(section_mid: str): - template = env().get_template( + output = env().render_template_as_markup( "actions/" "document/" "create_section/" - "stream_cancel_new_section.jinja.html" + "stream_cancel_new_section.jinja.html", + section_mid=section_mid, ) - output = template.render(section_mid=section_mid) return HTMLResponse( content=output, headers={ @@ -658,9 +642,6 @@ def cancel_edit_section(section_mid: str): section: SDocSection = export_action.traceability_index.get_node_by_mid( MID(section_mid) ) - template = env().get_template( - "actions/document/edit_section/stream_updated_section.jinja.html" - ) link_renderer = LinkRenderer( root_path=section.document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -682,7 +663,11 @@ def cancel_edit_section(section_mid: str): markup_renderer=markup_renderer, standalone=False, ) - output = template.render(view_object=view_object, node=section) + output = env().render_template_as_markup( + "actions/document/edit_section/stream_updated_section.jinja.html", + view_object=view_object, + node=section, + ) return HTMLResponse( content=output, status_code=200, @@ -752,12 +737,6 @@ def get_new_requirement( else: raise NotImplementedError - template = env().get_template( - "actions/" - "document/" - "create_requirement/" - "stream_new_requirement.jinja.html" - ) link_renderer = LinkRenderer( root_path=document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -770,7 +749,11 @@ def get_new_requirement( config=project_config, context_document=document, ) - output = template.render( + output = env().render_template_as_markup( + "actions/" + "document/" + "create_requirement/" + "stream_new_requirement.jinja.html", is_new_requirement=True, renderer=markup_renderer, form_object=form_object, @@ -823,12 +806,6 @@ def get_clone_requirement(reference_mid: str, context_document_mid: str): whereto = NodeCreationOrder.AFTER replace_action = "after" - template = env().get_template( - "actions/" - "document/" - "create_requirement/" - "stream_new_requirement.jinja.html" - ) link_renderer = LinkRenderer( root_path=document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -841,7 +818,11 @@ def get_clone_requirement(reference_mid: str, context_document_mid: str): config=project_config, context_document=document, ) - output = template.render( + output = env().render_template_as_markup( + "actions/" + "document/" + "create_requirement/" + "stream_new_requirement.jinja.html", is_new_requirement=True, renderer=markup_renderer, form_object=form_object, @@ -912,12 +893,6 @@ async def create_requirement(request: Request): command.perform() if form_object.any_errors(): - template = env().get_template( - "actions/" - "document/" - "create_requirement/" - "stream_new_requirement.jinja.html" - ) link_renderer = LinkRenderer( root_path=document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -930,8 +905,11 @@ async def create_requirement(request: Request): config=project_config, context_document=document, ) - - output = template.render( + output = env().render_template_as_markup( + "actions/" + "document/" + "create_requirement/" + "stream_new_requirement.jinja.html", is_new_requirement=True, renderer=markup_renderer, form_object=form_object, @@ -1009,12 +987,6 @@ def get_edit_requirement(node_id: str, context_document_mid: str): ) ) document = requirement.document - template = env().get_template( - "actions/" - "document/" - "edit_requirement/" - "stream_edit_requirement.jinja.html" - ) link_renderer = LinkRenderer( root_path=document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -1027,7 +999,11 @@ def get_edit_requirement(node_id: str, context_document_mid: str): config=project_config, context_document=document, ) - output = template.render( + output = env().render_template_as_markup( + "actions/" + "document/" + "edit_requirement/" + "stream_edit_requirement.jinja.html", is_new_requirement=False, renderer=markup_renderer, form_object=form_object, @@ -1074,13 +1050,10 @@ def reset_uid(reference_mid: str): field_mid=MID.create(), field_name="UID", field_type=RequirementFormFieldType.SINGLELINE, - field_unescaped_value=next_uid, - field_escaped_value=next_uid, + field_value=next_uid, ) - template = env().get_template( - "components/form/row/row_uid_with_reset/stream.jinja" - ) - output = template.render( + output = env().render_template_as_markup( + "components/form/row/row_uid_with_reset/stream.jinja", next_uid=next_uid, reference_mid=reference_mid, uid_form_field=uid_form_field, @@ -1146,12 +1119,6 @@ async def document__update_requirement(request: Request): update_requirement_command_result_or_none = update_command.perform() if form_object.any_errors(): - template = env().get_template( - "actions/" - "document/" - "edit_requirement/" - "stream_edit_requirement.jinja.html" - ) link_renderer = LinkRenderer( root_path=document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -1164,7 +1131,11 @@ async def document__update_requirement(request: Request): config=project_config, context_document=document, ) - output = template.render( + output = env().render_template_as_markup( + "actions/" + "document/" + "edit_requirement/" + "stream_edit_requirement.jinja.html", is_new_requirement=False, renderer=markup_renderer, requirement=requirement, @@ -1236,13 +1207,13 @@ async def document__update_requirement(request: Request): "/actions/document/cancel_new_requirement", response_class=Response ) def cancel_new_requirement(requirement_mid: str): - template = env().get_template( + output = env().render_template_as_markup( "actions/" "document/" "create_requirement/" - "stream_cancel_new_requirement.jinja.html" + "stream_cancel_new_requirement.jinja.html", + requirement_mid=requirement_mid, ) - output = template.render(requirement_mid=requirement_mid) return HTMLResponse( content=output, status_code=200, @@ -1320,11 +1291,9 @@ def delete_section( errors = [] except MultipleValidationErrorAsList as error_: errors = error_.errors - template = env().get_template( + output = env().render_template_as_markup( "actions/document/delete_section/" - "stream_confirm_delete_section.jinja" - ) - output = template.render( + "stream_confirm_delete_section.jinja", section_mid=node_id, context_document_mid=context_document_mid, errors=errors, @@ -1344,11 +1313,9 @@ def delete_section( delete_command.perform() except MultipleValidationErrorAsList as error_: errors = error_.errors - template = env().get_template( + output = env().render_template_as_markup( "actions/document/delete_section/" - "stream_confirm_delete_section.jinja" - ) - output = template.render( + "stream_confirm_delete_section.jinja", section_mid=node_id, context_document_mid=context_document_mid, errors=errors, @@ -1372,9 +1339,6 @@ def delete_section( SDWriter(project_config).write_to_file(section.document) # Rendering back the Turbo template. - template = env().get_template( - "actions/document/delete_section/stream_delete_section.jinja.html" - ) link_renderer = LinkRenderer( root_path=section.document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -1396,11 +1360,14 @@ def delete_section( markup_renderer=markup_renderer, standalone=False, ) - output = template.render(view_object=view_object) - toc_template = env().get_template( - "actions/document/_shared/stream_updated_toc.jinja.html" + output = env().render_template_as_markup( + "actions/document/delete_section/stream_delete_section.jinja.html", + view_object=view_object, + ) + output += env().render_template_as_markup( + "actions/document/_shared/stream_updated_toc.jinja.html", + view_object=view_object, ) - output += toc_template.render(view_object=view_object) return HTMLResponse( content=output, status_code=200, @@ -1431,11 +1398,9 @@ def delete_requirement( except MultipleValidationErrorAsList as error_: errors = error_.errors - template = env().get_template( + output = env().render_template_as_markup( "actions/document/delete_requirement/" - "stream_confirm_delete_requirement.jinja" - ) - output = template.render( + "stream_confirm_delete_requirement.jinja", requirement_mid=node_id, context_document_mid=context_document_mid, errors=errors, @@ -1455,9 +1420,8 @@ def delete_requirement( ) delete_command.perform() except MultipleValidationError: - output = "" return HTMLResponse( - content=output, + content="", status_code=422, headers={ "Content-Type": "text/vnd.turbo-stream.html", @@ -1474,10 +1438,6 @@ def delete_requirement( ) # Rendering back the Turbo template. - template = env().get_template( - "actions/document/delete_requirement/" - "stream_delete_requirement.jinja.html" - ) link_renderer = LinkRenderer( root_path=requirement.document.meta.get_root_path_prefix(), static_path=project_config.dir_for_sdoc_assets, @@ -1499,12 +1459,16 @@ def delete_requirement( markup_renderer=markup_renderer, standalone=False, ) - output = template.render(view_object=view_object) + output = env().render_template_as_markup( + "actions/document/delete_requirement/" + "stream_delete_requirement.jinja.html", + view_object=view_object, + ) - toc_template = env().get_template( - "actions/document/_shared/stream_updated_toc.jinja.html" + output += env().render_template_as_markup( + "actions/document/_shared/stream_updated_toc.jinja.html", + view_object=view_object, ) - output += toc_template.render(view_object=view_object) return HTMLResponse( content=output, status_code=200, @@ -1611,10 +1575,8 @@ async def move_node(request: Request): # Generic routes @router.get("/actions/project_index/new_document", response_class=Response) def get_new_document(): - template = env().get_template( - "actions/project_index/stream_new_document.jinja.html" - ) - output = template.render( + output = env().render_template_as_markup( + "actions/project_index/stream_new_document.jinja.html", error_object=ErrorObject(), document_title="", document_path="", @@ -1682,10 +1644,8 @@ def document_tree__create_document( ) if error_object.any_errors(): - template = env().get_template( - "actions/project_index/stream_new_document.jinja.html" - ) - output = template.render( + output = env().render_template_as_markup( + "actions/project_index/stream_new_document.jinja.html", error_object=error_object, document_title=document_title if document_title is not None @@ -1757,16 +1717,14 @@ def document_tree__create_document( export_action.build_index() export_action.export() - template = env().get_template( - "actions/project_index/stream_create_document.jinja.html" - ) - view_object = ProjectTreeViewObject( traceability_index=export_action.traceability_index, project_config=project_config, ) - - output = template.render(view_object=view_object) + output = env().render_template_as_markup( + "actions/project_index/stream_create_document.jinja.html", + view_object=view_object, + ) return HTMLResponse( content=output, status_code=200, @@ -1787,15 +1745,13 @@ def document__add_comment( ) assert document.grammar is not None grammar: DocumentGrammar = document.grammar - template = env().get_template( + # The data of the form object is ignored. What matters is the comment + # form data. + output = env().render_template_as_markup( "actions/" "document/" "add_requirement_comment/" - "stream_add_requirement_comment.jinja.html" - ) - # The data of the form object is ignored. What matters is the comment - # form data. - output = template.render( + "stream_add_requirement_comment.jinja.html", requirement_mid=requirement_mid, form_object=RequirementFormObject( is_new=False, @@ -1814,8 +1770,7 @@ def document__add_comment( field_mid=MID.create(), field_name="COMMENT", field_type=RequirementFormFieldType.MULTILINE, - field_unescaped_value="", - field_escaped_value="", + field_value="", ), ) return HTMLResponse( @@ -1842,15 +1797,13 @@ def document__add_relation( grammar_element_relations = element.get_relation_types() - template = env().get_template( + # The data of the form object is ignored. What matters is the relation + # form data. + output = env().render_template_as_markup( "actions/" "document/" "add_requirement_relation/" - "stream_add_requirement_relation.jinja.html" - ) - # The data of the form object is ignored. What matters is the relation - # form data. - output = template.render( + "stream_add_requirement_relation.jinja.html", requirement_mid=requirement_mid, form_object=RequirementFormObject( is_new=False, @@ -1890,13 +1843,11 @@ def document__edit_config(document_mid: str): document=document ) - template = env().get_template( + output = env().render_template_as_markup( "actions/" "document/" "edit_document_config/" - "stream_edit_document_config.jinja.html" - ) - output = template.render( + "stream_edit_document_config.jinja.html", form_object=form_object, document=document, ) @@ -1955,13 +1906,11 @@ async def document__save_edit_config(request: Request): for error_key, errors in validation_error.errors.items(): for error in errors: form_object.add_error(error_key, error) - template = env().get_template( + html_output = env().render_template_as_markup( "actions/" "document/" "edit_document_config/" - "stream_edit_document_config.jinja.html" - ) - html_output: str = template.render( + "stream_edit_document_config.jinja.html", form_object=form_object, document=document, ) @@ -2002,13 +1951,13 @@ async def document__save_edit_config(request: Request): markup_renderer=markup_renderer, standalone=False, ) - template = env().get_template( + html_output = env().render_template_as_markup( "actions/" "document/" "edit_document_config/" - "stream_save_document_config.jinja.html" + "stream_save_document_config.jinja.html", + view_object=view_object, ) - html_output = template.render(view_object=view_object) return HTMLResponse( content=html_output, status_code=200, @@ -2113,12 +2062,6 @@ def document__cancel_edit_config(document_mid: str): config=project_config, context_document=document, ) - template = env().get_template( - "actions/" - "document/" - "edit_document_config/" - "stream_cancel_edit_document_config.jinja.html" - ) view_object = DocumentScreenViewObject( document_type=DocumentType.document(), document=document, @@ -2128,7 +2071,14 @@ def document__cancel_edit_config(document_mid: str): markup_renderer=markup_renderer, standalone=False, ) - output = template.render(view_object=view_object, document=document) + output = env().render_template_as_markup( + "actions/" + "document/" + "edit_document_config/" + "stream_cancel_edit_document_config.jinja.html", + view_object=view_object, + document=document, + ) return HTMLResponse( content=output, status_code=200, @@ -2157,9 +2107,6 @@ def document__cancel_edit_included_document(document_mid: str): config=project_config, context_document=document, ) - template = env().get_template( - "actions/document/edit_section/stream_updated_section.jinja.html" - ) view_object = DocumentScreenViewObject( document_type=DocumentType.document(), document=document, @@ -2169,8 +2116,11 @@ def document__cancel_edit_included_document(document_mid: str): markup_renderer=markup_renderer, standalone=False, ) - output = template.render( - view_object=view_object, document=document, node=document + output = env().render_template_as_markup( + "actions/document/edit_section/stream_updated_section.jinja.html", + view_object=view_object, + document=document, + node=document, ) return HTMLResponse( content=output, @@ -2274,14 +2224,15 @@ async def document__save_grammar(request: Request): markup_renderer=markup_renderer, standalone=False, ) - template = env().get_template( - "actions/" - "document/" - "_shared/" - "stream_refresh_document.jinja.html" - ) - output = form_object.render_close_form() + template.render( - view_object=view_object + output = ( + form_object.render_close_form() + + env().render_template_as_markup( + "actions/" + "document/" + "_shared/" + "stream_refresh_document.jinja.html", + view_object=view_object, + ) ) return HTMLResponse( content=output, @@ -2415,14 +2366,15 @@ async def document__save_grammar_element(request: Request): markup_renderer=markup_renderer, standalone=False, ) - template = env().get_template( - "actions/" - "document/" - "_shared/" - "stream_refresh_document.jinja.html" - ) - output = form_object.render_close_form() + template.render( - view_object=view_object + output = ( + form_object.render_close_form() + + env().render_template_as_markup( + "actions/" + "document/" + "_shared/" + "stream_refresh_document.jinja.html", + view_object=view_object, + ) ) return HTMLResponse( content=output, @@ -2477,11 +2429,9 @@ def document__add_grammar_relation(document_mid: str): response_class=Response, ) def get_import_reqif_document_form(): - template = env().get_template( + output = env().render_template_as_markup( "actions/project_index/import_reqif_document/" - "stream_form_import_reqif_document.jinja.html" - ) - output = template.render( + "stream_form_import_reqif_document.jinja.html", error_object=ErrorObject(), ) return HTMLResponse( @@ -2518,11 +2468,9 @@ async def import_document_reqif(reqif_file: UploadFile): error_object.add_error("reqif_file", str(exception)) if error_object.any_errors(): - template = env().get_template( + output = env().render_template_as_markup( "actions/project_index/import_reqif_document/" - "stream_form_import_reqif_document.jinja.html" - ) - output = template.render( + "stream_form_import_reqif_document.jinja.html", error_object=error_object, ) return HTMLResponse( @@ -2574,15 +2522,15 @@ async def import_document_reqif(reqif_file: UploadFile): export_action.build_index() export_action.export() - template = env().get_template( - "actions/project_index/import_reqif_document/" - "stream_refresh_with_imported_reqif_document.jinja.html" - ) view_object = ProjectTreeViewObject( traceability_index=export_action.traceability_index, project_config=project_config, ) - output = template.render(view_object=view_object) + output = env().render_template_as_markup( + "actions/project_index/import_reqif_document/" + "stream_refresh_with_imported_reqif_document.jinja.html", + view_object=view_object, + ) return HTMLResponse( content=output, status_code=200, @@ -2722,14 +2670,12 @@ def get_search(q: Optional[str] = None): except (AttributeError, NameError, TypeError) as attribute_error_: error = attribute_error_.args[0] - search_value = html.escape(q) if q is not None else "" - view_object = SearchScreenViewObject( traceability_index=export_action.traceability_index, project_config=project_config, templates=html_templates, search_results=search_results, - search_value=search_value, + search_value=q if q is not None else "", error=error, ) output = view_object.render_screen(html_templates.jinja_environment()) diff --git a/tests/end2end/project_index/import_document_from_reqif/UC55_G1_validations/UC55_G1_T01_not_a_reqif_format/test_UC55_G1_T01_not_a_reqif_file.py b/tests/end2end/project_index/import_document_from_reqif/UC55_G1_validations/UC55_G1_T01_not_a_reqif_format/test_UC55_G1_T01_not_a_reqif_file.py index a3961f8c4..3b001901b 100644 --- a/tests/end2end/project_index/import_document_from_reqif/UC55_G1_validations/UC55_G1_T01_not_a_reqif_format/test_UC55_G1_T01_not_a_reqif_file.py +++ b/tests/end2end/project_index/import_document_from_reqif/UC55_G1_validations/UC55_G1_T01_not_a_reqif_format/test_UC55_G1_T01_not_a_reqif_file.py @@ -33,5 +33,5 @@ def test(self): form_import.do_form_submit_and_catch_error( "Cannot parse ReqIF file: " - "Start tag expected, '<' not found, line 1, column 1 (, line 1)" + "Start tag expected, '<' not found, line 1, column 1 (, line 1)" ) diff --git a/tests/integration/options/options_per_document/MARKUP/02_options_markup_is_text/test.itest b/tests/integration/options/options_per_document/MARKUP/02_options_markup_is_text/test.itest index ee7253725..d3663fe73 100644 --- a/tests/integration/options/options_per_document/MARKUP/02_options_markup_is_text/test.itest +++ b/tests/integration/options/options_per_document/MARKUP/02_options_markup_is_text/test.itest @@ -3,4 +3,4 @@ CHECK: Published: Hello world doc RUN: %cat %S/Output/html/02_options_markup_is_text/input.html | filecheck %s --dump-input=fail --check-prefix CHECK-HTML CHECK-HTML: **This text will not be converted to strong tag** -CHECK-HTML: <a href="url">link</a> +CHECK-HTML: <a href="url">link</a> diff --git a/tests/unit/strictdoc/export/html/renderers/test_text_to_html_fragment_writer.py b/tests/unit/strictdoc/export/html/renderers/test_text_to_html_fragment_writer.py index 4d85db40c..da7196a03 100644 --- a/tests/unit/strictdoc/export/html/renderers/test_text_to_html_fragment_writer.py +++ b/tests/unit/strictdoc/export/html/renderers/test_text_to_html_fragment_writer.py @@ -7,7 +7,7 @@ def test_01_escapes_html_tags(): """.strip() html_output = TextToHtmlWriter.write(text_input) - assert "<a href="url">link</a>" == html_output + assert "<a href="url">link</a>" == html_output def test_02_replaces_newlines_with_br():