-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
export/dot: basic export to Graphviz/DOT
- Loading branch information
Showing
21 changed files
with
398 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import os | ||
from pathlib import Path | ||
from typing import Union | ||
|
||
import graphviz | ||
|
||
from strictdoc.backend.sdoc.models.document import Document | ||
from strictdoc.backend.sdoc.models.requirement import Requirement | ||
from strictdoc.backend.sdoc.models.section import Section | ||
from strictdoc.core.document_iterator import DocumentCachingIterator | ||
from strictdoc.core.traceability_index import TraceabilityIndex | ||
from strictdoc.export.dot.dot_templates import DotTemplates | ||
|
||
|
||
def get_path_components(folder_path): | ||
path = os.path.normpath(folder_path) | ||
return path.split(os.sep) | ||
|
||
|
||
class DocumentDotGenerator: | ||
def __init__(self, profile: str): | ||
assert profile in ("profile1", "profile2"), profile | ||
self.profile: str = profile | ||
self.index_template = DotTemplates.jinja_environment.get_template( | ||
f"{profile}/top_level.dot" | ||
) | ||
self.template_section = DotTemplates.jinja_environment.get_template( | ||
f"{profile}/section.dot" | ||
) | ||
self.template_requirement = DotTemplates.jinja_environment.get_template( | ||
f"{profile}/requirement.dot" | ||
) | ||
|
||
def export_tree( | ||
self, traceability_index: TraceabilityIndex, output_dot_root | ||
): | ||
Path(output_dot_root).mkdir(parents=True, exist_ok=True) | ||
|
||
project_tree_content = "" | ||
|
||
accumulated_links = [] | ||
accumulated_section_siblings = [] | ||
document_flat_requirements = [] | ||
document: Document | ||
for document in traceability_index.document_tree.document_list: | ||
if not document.has_any_requirements(): | ||
continue | ||
document_content = self._print_node( | ||
document, accumulated_links, accumulated_section_siblings | ||
) | ||
project_tree_content += document_content | ||
project_tree_content += "\n\n" | ||
|
||
this_document_flat_requirements = [] | ||
|
||
iterator = DocumentCachingIterator(document) | ||
for node in iterator.all_content(): | ||
if isinstance(node, Requirement): | ||
uuid = self.get_requirement_uuid(node) | ||
|
||
this_document_flat_requirements.append(f'"value_{uuid}"') | ||
|
||
this_document_flat_requirements_str = " -> ".join( | ||
this_document_flat_requirements | ||
) | ||
document_flat_requirements.append( | ||
this_document_flat_requirements_str | ||
) | ||
|
||
dot_output = self.index_template.render( | ||
project_tree_content=project_tree_content, | ||
accumulated_links=accumulated_links, | ||
accumulated_section_siblings=accumulated_section_siblings, | ||
document_flat_requirements=document_flat_requirements, | ||
) | ||
|
||
output_path = os.path.join( | ||
output_dot_root, f"output.{self.profile}.dot" | ||
) | ||
with open(output_path, "w", encoding="utf8") as file: | ||
file.write(dot_output) | ||
|
||
dot = graphviz.Source.from_file(output_path) | ||
# view=True makes the output PDF be opened in a default viewer program. | ||
dot.render(output_path, view=False) | ||
|
||
def _print_node( | ||
self, | ||
node: Union[Document, Section], | ||
accumulated_links, | ||
accumulated_section_siblings, | ||
) -> str: | ||
def get_uuid(node_): | ||
return node_.node_id | ||
|
||
def get_upper_sibling_section(node_: Section): | ||
parent: Union[Document, Section] = node_.parent | ||
node_index = parent.section_contents.index(node_) | ||
if node_index > 0: | ||
candidate_upper_sibling = parent.section_contents[ | ||
node_index - 1 | ||
] | ||
if isinstance(candidate_upper_sibling, Section): | ||
return candidate_upper_sibling | ||
return None | ||
|
||
node_content = "" | ||
for subnode in node.section_contents: | ||
if isinstance(subnode, Section): | ||
node_content += self._print_node( | ||
subnode, accumulated_links, accumulated_section_siblings | ||
) | ||
node_content += "\n" | ||
|
||
upper_sibling_section = get_upper_sibling_section(subnode) | ||
if upper_sibling_section is not None: | ||
accumulated_section_siblings.append( | ||
(get_uuid(subnode), get_uuid(upper_sibling_section)) | ||
) | ||
elif isinstance(subnode, Requirement): | ||
node_content += self._print_requirement_fields( | ||
subnode, accumulated_links | ||
) | ||
node_content += "\n" | ||
output = self.template_section.render( | ||
section=node, | ||
uuid=get_uuid(node), | ||
node_content=node_content, | ||
font_size=32 - node.ng_level * 4, | ||
context_title=f"{node.context.title_number_string} {node.title}" | ||
if isinstance(node, Section) | ||
else node.title, | ||
) | ||
return output | ||
|
||
def _print_requirement_fields( | ||
self, requirement: Requirement, accumulated_links | ||
): | ||
uuid = self.get_requirement_uuid(requirement) | ||
for parent_uid in requirement.get_parent_requirement_reference_uids(): | ||
accumulated_links.append((uuid, parent_uid)) | ||
output = self.template_requirement.render( | ||
requirement=requirement, | ||
uuid=uuid, | ||
) | ||
return output | ||
|
||
@staticmethod | ||
def get_requirement_uuid(requirement: Requirement): | ||
assert isinstance(requirement, Requirement) | ||
return ( | ||
requirement.reserved_uid | ||
if requirement.reserved_uid is not None | ||
else requirement.node_id | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from jinja2 import Environment, FileSystemLoader, StrictUndefined | ||
|
||
from strictdoc import environment | ||
|
||
|
||
class DotTemplates: | ||
jinja_environment = Environment( | ||
loader=FileSystemLoader(environment.get_path_to_dot_templates()), | ||
undefined=StrictUndefined, | ||
) | ||
# TODO: Check if this line is still needed (might be some older workaround). | ||
jinja_environment.globals.update(isinstance=isinstance) | ||
jinja_environment.trim_blocks = False | ||
jinja_environment.lstrip_blocks = False | ||
jinja_environment.strip_trailing_newlines = False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Requirement node | ||
node [fontsize=16, fontname="Arial"]; | ||
|
||
// \l makes the label text be left-aligned. | ||
// Without fontsize=16 fontname="Arial" defined below, the font gets corrupted, | ||
// when a node has edges connected to it. | ||
"value_{{uuid}}" [ | ||
label = "{{requirement.reserved_uid}} {{requirement.reserved_title}}\l" | ||
width=7.0 height=1.0 fontsize=16 fontname="Arial" | ||
penwidth=1 | ||
style="rounded" | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// Section node: {{section.reserved_uid}} {{section.title}} | ||
subgraph "cluster_{{uuid}}" { | ||
margin = "24"; | ||
label = "{{context_title}}"; | ||
fontsize = {{font_size}}; | ||
fontname="Arial-bold"; | ||
penwidth=1; | ||
style="rounded"; | ||
|
||
labeljust="l"; | ||
|
||
|
||
{% filter indent(width=2) %} | ||
{{node_content}} | ||
{% endfilter %} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
digraph { | ||
// layout=fdp; | ||
// rankdir=LR; | ||
node [shape=box]; | ||
|
||
// If true, allow edges between clusters | ||
compound=true; | ||
// ordering=out; | ||
// outputorder=nodesfirst; | ||
// newrank = true; | ||
|
||
// Make the links have orthogonal shapes. The best approach so far to make the | ||
// nodes be connected with the shortest edges. | ||
// SO: How can I direct dot to use a shorter edge path? | ||
// https://stackoverflow.com/q/24107451/598057 | ||
splines=ortho; | ||
|
||
// Ensure some space between documents. | ||
graph [ranksep=1]; | ||
|
||
{% filter indent(width=2) %} | ||
{{project_tree_content}} | ||
{% endfilter %} | ||
|
||
{% for accumulated_link in accumulated_links -%} | ||
"value_{{accumulated_link[0]}}"->"value_{{accumulated_link[1]}}" [ | ||
style="dotted" color="#000000", penwidth = 2, constraint=false | ||
]; | ||
{%- endfor %} | ||
|
||
{% for single_document_flat_requirements in document_flat_requirements -%} | ||
{{single_document_flat_requirements}} [style=invis]; | ||
{%- endfor %} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Requirement node | ||
node [fontsize=16, fontname="Arial"]; | ||
|
||
// \l makes the label text be left-aligned. | ||
// Without fontsize=16 fontname="Arial" defined below, the font gets corrupted, | ||
// when a node has edges connected to it. | ||
"value_{{uuid}}" [ | ||
label = "{{requirement.reserved_title}}\l" | ||
width=7.0 height=1.0 fontsize=16 fontname="Arial" | ||
penwidth=1 | ||
style="rounded" | ||
]; |
Oops, something went wrong.