Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
*.idea
/.idea/
/.venv/
/dist/

**/build/
*.egg-info/
*.pyc
*.script
*.venv
dist
output.zip
*.sketch
.DS_Store
setup.py
.vscode
Expand Down
56 changes: 47 additions & 9 deletions reqif/helpers/lxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from itertools import chain

from lxml import etree
from lxml.etree import _Comment, tostring
from lxml.etree import Comment, tostring
from lxml.html import fragment_fromstring


def lxml_dump_node(node):
return lxml_stringify_node(node)
return lxml_stringify_node(node, root_node=False)


# This code is taken from Python 3.7. The addition is escaping of the tab
Expand All @@ -25,6 +25,8 @@ def lxml_escape_for_html(string: str) -> str:
string = string.replace(">", ">")
string = string.replace('"', """)
string = string.replace("'", "'")
# Non-breaking space character.
string = string.replace("\xa0", " ")
# Invisible tab character
string = string.replace("\t", "	")
return string
Expand Down Expand Up @@ -120,16 +122,53 @@ def _lxml_stringify_reqif_ns_node(node):
return string


def lxml_stringify_node(node):
def lxml_stringify_node(node, root_node=True):
"""
Stringify a given LXML node.

:param node:
:param root_node: Needed to track whether a node is the first among the
nodes being stringified. Some ReqIF producers do not use a
global xmlns="http://www.w3.org/1999/xhtml" namespace.
Instead, they assign this namespace only to the very first
node inside the ATTRIBUTE-VALUE-XHTML/THE-VALUE tag, for
example: <div xmlns="http://www.w3.org/1999/xhtml">...
Tracking this root node ensures that the xmlns attribute
is assigned only to the first node and not to all
subsequent nodes.
:return:
"""
output = ""

# Some ReqIF producers add <!-- --> comments but these comment nodes
# cannot be handled using etree.QName(node).localname like used further
# below. Handling them separately with this dedicated branch.
# A user report that helped to discover this case:
# https://github.com/strictdoc-project/reqif/issues/205
if lxml_is_comment_node(node):
assert node.text is not None
output = f"<!--{node.text}-->"
if node.tail is not None:
output += lxml_escape_for_html(node.tail)
return output

nskey = None
nsvalue = None
if len(node.nsmap) > 0:
nskey = next(iter(node.nsmap.keys()))
output = ""
nskey, nsvalue = next(iter(node.nsmap.items()))

node_no_ns_tag = etree.QName(node).localname
tag = f"{nskey}:{node_no_ns_tag}" if node.tag[0] == "{" else node.tag
tag = (
f"{nskey}:{node_no_ns_tag}"
if node.tag[0] == "{" and nskey is not None
else node_no_ns_tag
)
output += f"<{tag}"
for attribute, attribute_value in node.attrib.items():
output += f' {attribute}="{lxml_escape_for_html(attribute_value)}"'
if nsvalue is not None and root_node:
output += f' xmlns="{nsvalue}"'

# <object> is surprisingly a tag that must have a closing tag even if it
# is empty. If self-closed, it breaks all the following markup.
if (
Expand All @@ -141,7 +180,7 @@ def lxml_stringify_node(node):
if node.text is not None:
output += lxml_escape_for_html(node.text)
for child in node.getchildren():
output += lxml_stringify_node(child)
output += lxml_stringify_node(child, root_node=False)
output += f"</{tag}>"
else:
output += "/>"
Expand Down Expand Up @@ -221,5 +260,4 @@ def lxml_strip_namespace_from_xml(root_xml, full=False):


def lxml_is_comment_node(xml_node):
# FIXME: Accessing a "_"-marked Comment class of lxml is not great.
return isinstance(xml_node, _Comment)
return xml_node.tag is Comment
91 changes: 91 additions & 0 deletions tests/integration/reqif/_other/01_tinymce_comments/sample.reqif
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<REQ-IF xmlns="http://www.omg.org/spec/ReqIF/20110401/reqif.xsd">
<THE-HEADER>
<REQ-IF-HEADER IDENTIFIER="header_id">
<CREATION-TIME>2023-01-01T00:00:00.000Z</CREATION-TIME>
<REQ-IF-TOOL-ID>Test Tool</REQ-IF-TOOL-ID>
<REQ-IF-VERSION>1.0</REQ-IF-VERSION>
<SOURCE-TOOL-ID>Test</SOURCE-TOOL-ID>
<TITLE>Minimal ReqIF</TITLE>
</REQ-IF-HEADER>
</THE-HEADER>
<CORE-CONTENT>
<REQ-IF-CONTENT>
<DATATYPES>
<DATATYPE-DEFINITION-STRING IDENTIFIER="DT_String" LONG-NAME="String" MAX-LENGTH="1000">
</DATATYPE-DEFINITION-STRING>
</DATATYPES>
<SPEC-TYPES>
<SPEC-OBJECT-TYPE IDENTIFIER="SOT_Req" LONG-NAME="Requirement">
<SPEC-ATTRIBUTES>
<ATTRIBUTE-DEFINITION-STRING IDENTIFIER="AD_ForeignID" LONG-NAME="ReqIF.ForeignID">
<TYPE>
<DATATYPE-DEFINITION-STRING-REF>DT_String</DATATYPE-DEFINITION-STRING-REF>
</TYPE>
</ATTRIBUTE-DEFINITION-STRING>
</SPEC-ATTRIBUTES>
</SPEC-OBJECT-TYPE>
<SPECIFICATION-TYPE IDENTIFIER="ST_Spec" LAST-CHANGE="2023-01-01T00:00:00.000Z" LONG-NAME="Specification">
<SPEC-ATTRIBUTES>
<ATTRIBUTE-DEFINITION-STRING IDENTIFIER="AD_SpecName" LONG-NAME="Name">
<TYPE>
<DATATYPE-DEFINITION-STRING-REF>DT_String</DATATYPE-DEFINITION-STRING-REF>
</TYPE>
</ATTRIBUTE-DEFINITION-STRING>
</SPEC-ATTRIBUTES>
</SPECIFICATION-TYPE>
</SPEC-TYPES>
<SPEC-OBJECTS>
<SPEC-OBJECT IDENTIFIER="_73d9cdc2-1c71-4d27-92ab-1df19c9f6ac3" LAST-CHANGE="2025-10-15T05:15:32.902Z">
<TYPE>
<SPEC-OBJECT-TYPE-REF>_6cf52001-6b78-428a-bfcd-429ba136d53a</SPEC-OBJECT-TYPE-REF>
</TYPE>
<VALUES>
<ATTRIBUTE-VALUE-XHTML>
<DEFINITION>
<ATTRIBUTE-DEFINITION-XHTML-REF>_0cad5cb5-6b09-4b60-bb66-fed63d56e806</ATTRIBUTE-DEFINITION-XHTML-REF>
</DEFINITION>
<THE-VALUE><div xmlns="http://www.w3.org/1999/xhtml">
<p><!-- x-tinymce/html -->The&#xA0;###<!-- x-tinymce/html -->Platform shall&#xA0; provide cover art within 2s of audio select</p>
</div></THE-VALUE>
</ATTRIBUTE-VALUE-XHTML>
<ATTRIBUTE-VALUE-STRING THE-VALUE="1672600">
<DEFINITION>
<ATTRIBUTE-DEFINITION-STRING-REF>_d0bf9a76-226d-4856-9387-e64040705955</ATTRIBUTE-DEFINITION-STRING-REF>
</DEFINITION>
</ATTRIBUTE-VALUE-STRING>
<ATTRIBUTE-VALUE-ENUMERATION>
<DEFINITION>
<ATTRIBUTE-DEFINITION-ENUMERATION-REF>_4d6b67beb8b9fa04468c6c2d5ae99f484d02c5d8</ATTRIBUTE-DEFINITION-ENUMERATION-REF>
</DEFINITION>
<VALUES>
<ENUM-VALUE-REF>_009812fe2a185feb11f17bd36dcd508a736fe8c8</ENUM-VALUE-REF>
</VALUES>
</ATTRIBUTE-VALUE-ENUMERATION>
</VALUES>
</SPEC-OBJECT>
</SPEC-OBJECTS>
<SPECIFICATIONS>
<SPECIFICATION IDENTIFIER="SPEC_Main" LONG-NAME="Main Specification">
<VALUES>
<ATTRIBUTE-VALUE-STRING THE-VALUE="Test Specification">
<DEFINITION>
<ATTRIBUTE-DEFINITION-STRING-REF>AD_SpecName</ATTRIBUTE-DEFINITION-STRING-REF>
</DEFINITION>
</ATTRIBUTE-VALUE-STRING>
</VALUES>
<TYPE>
<SPECIFICATION-TYPE-REF>ST_Spec</SPECIFICATION-TYPE-REF>
</TYPE>
<CHILDREN>
<SPEC-HIERARCHY IDENTIFIER="SH_001" LONG-NAME="Requirement Hierarchy">
<OBJECT>
<SPEC-OBJECT-REF>_73d9cdc2-1c71-4d27-92ab-1df19c9f6ac3</SPEC-OBJECT-REF>
</OBJECT>
</SPEC-HIERARCHY>
</CHILDREN>
</SPECIFICATION>
</SPECIFICATIONS>
</REQ-IF-CONTENT>
</CORE-CONTENT>
</REQ-IF>
3 changes: 3 additions & 0 deletions tests/integration/reqif/_other/01_tinymce_comments/test.itest
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RUN: mkdir -p %S/output
RUN: %reqif passthrough %S/sample.reqif %S/output/sample.reqif
RUN: %diff %S/sample.reqif %S/output/sample.reqif