From 4fe8e26052de72d771dc9a80fea8d6eec9e7ce67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Fri, 16 Jan 2026 19:03:33 +0100 Subject: [PATCH 01/11] Added image support --- examples/generate_images.sh | 4 ++ examples/images.tmplt | 19 ++++++ examples/test_image1.png | Bin 0 -> 23306 bytes examples/test_image2.jpg | Bin 0 -> 17650 bytes templateprocessor/md2docx.py | 44 +++++++++--- templateprocessor/postprocessor.py | 4 -- tests/test_md2docx.py | 106 +++++++++++++++++++++++++++++ 7 files changed, 164 insertions(+), 13 deletions(-) create mode 100755 examples/generate_images.sh create mode 100644 examples/images.tmplt create mode 100644 examples/test_image1.png create mode 100644 examples/test_image2.jpg diff --git a/examples/generate_images.sh b/examples/generate_images.sh new file mode 100755 index 0000000..e8ecefc --- /dev/null +++ b/examples/generate_images.sh @@ -0,0 +1,4 @@ +#!/bin/bash +mkdir -p output + +template-processor --verbosity info -t images.tmplt -o output -p md2docx diff --git a/examples/images.tmplt b/examples/images.tmplt new file mode 100644 index 0000000..9acfe24 --- /dev/null +++ b/examples/images.tmplt @@ -0,0 +1,19 @@ +# Image Support Example + +This document demonstrates image support in the template processor. + +## Image Without Title + +Here is the first image without a title: + +![](test_image1.png) + +## Image With Title + +Here is the second image with a title: + +![test image 2](test_image2.jpg "Example Title for Image 2") + +## End + +This concludes the image support demonstration. diff --git a/examples/test_image1.png b/examples/test_image1.png new file mode 100644 index 0000000000000000000000000000000000000000..4b7e23ddc4b29e6f518a3891465ccbd9770ca300 GIT binary patch literal 23306 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 U8h_fslilZ#Qw6<2tKSy>56&E-Hvj+t literal 0 HcmV?d00001 diff --git a/templateprocessor/md2docx.py b/templateprocessor/md2docx.py index 56588b1..472300f 100644 --- a/templateprocessor/md2docx.py +++ b/templateprocessor/md2docx.py @@ -14,9 +14,13 @@ """ import markdown2 +import os from docx import Document +from docx.shared import Inches from bs4 import BeautifulSoup, Tag +IMAGE_WIDTH_IN_INCHES = 6 + def get_element_text(element: Tag) -> str: if hasattr(element, "get_text"): @@ -50,12 +54,28 @@ def process_list_items(list_element: Tag, doc: Document, style_base: str, level= if nested_ol: process_list_items(nested_ol, doc, "List Number", level + 1) +def embed_image(img : Tag, doc: Document): + img_src = img.get("src") + img_title = img.get("title", "").strip() + img_alt = img.get("alt", "").strip() + + # Use title if available, otherwise use alt text + caption_text = img_title if img_title else img_alt + + if img_src and os.path.exists(img_src): + try: + doc.add_picture(img_src, width=Inches(IMAGE_WIDTH_IN_INCHES)) + if caption_text: + caption_paragraph = doc.add_paragraph(caption_text) + caption_paragraph.style = "Caption" + except Exception: + # If image cannot be added, skip it silently + pass def markdown_to_word_file(markdown_source: str, word_file_path: str): doc = markdown_to_word_object(markdown_source) doc.save(word_file_path) - def markdown_to_word_object(markdown_source: str) -> Document: # Converting Markdown to HTML html_content = markdown2.markdown(markdown_source, extras=["tables", "wiki-tables"]) @@ -75,14 +95,20 @@ def markdown_to_word_object(markdown_source: str) -> Document: elif element.name == "h3": doc.add_heading(element.text, level=3) elif element.name == "p": - paragraph = doc.add_paragraph() - for child in element.children: - if child.name == "strong": - paragraph.add_run(child.text).bold = True - elif child.name == "em": - paragraph.add_run(child.text).italic = True - else: - paragraph.add_run(child) + # Check if paragraph contains an image + img = element.find("img") + if img: + embed_image(img, doc) + else: + # Regular paragraph without image + paragraph = doc.add_paragraph() + for child in element.children: + if child.name == "strong": + paragraph.add_run(child.text).bold = True + elif child.name == "em": + paragraph.add_run(child.text).italic = True + else: + paragraph.add_run(child) elif element.name == "ul": process_list_items(element, doc, "List Bullet") elif element.name == "ol": diff --git a/templateprocessor/postprocessor.py b/templateprocessor/postprocessor.py index ccee679..f2e7335 100644 --- a/templateprocessor/postprocessor.py +++ b/templateprocessor/postprocessor.py @@ -18,7 +18,6 @@ class PostprocessorType(Enum): class AbstractPostprocessor(ABC): - @abstractmethod def process(self, text: str, base_file_name: str) -> None: """ @@ -32,14 +31,12 @@ def process(self, text: str, base_file_name: str) -> None: class Md2docxPostprocessor(AbstractPostprocessor): - def process(self, text: str, base_file_name: str) -> None: output_file_name = f"{base_file_name}.docx" md2docx.markdown_to_word_file(text, output_file_name) class Md2HtmlPostprocessor(AbstractPostprocessor): - def process(self, text: str, base_file_name: str) -> None: output_file_name = f"{base_file_name}.html" html_content = markdown2.markdown(text, extras=["tables", "wiki-tables"]) @@ -48,7 +45,6 @@ def process(self, text: str, base_file_name: str) -> None: class PassthroughPostprocessor(AbstractPostprocessor): - def process(self, text: str, base_file_name: str) -> None: output_file_name = f"{base_file_name}.md" with open(output_file_name, "w") as f: diff --git a/tests/test_md2docx.py b/tests/test_md2docx.py index d122a81..2a8a883 100644 --- a/tests/test_md2docx.py +++ b/tests/test_md2docx.py @@ -2,9 +2,12 @@ Tests for md2docx module """ +import os +import tempfile import pytest from docx.document import Document as DocumentType from templateprocessor.md2docx import markdown_to_word_object +from PIL import Image class TestMarkdownToWordObject: @@ -150,3 +153,106 @@ def test_header(self): assert "Heading 1" in paragraphs[0].style.name assert "Heading 2" in paragraphs[1].style.name assert "Heading 3" in paragraphs[2].style.name + + def test_image_without_title(self): + """Test converting markdown with an image without a title.""" + # Create a temporary test image + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_img: + img = Image.new("RGB", (100, 100), color="red") + img.save(tmp_img.name) + tmp_img_path = tmp_img.name + + try: + # Prepare markdown with image reference + markdown = f"![alt text]({tmp_img_path})" + + # Execute + doc = markdown_to_word_object(markdown) + + # Verify + assert isinstance(doc, DocumentType) + # Should have the image added + # Check that there are inline shapes (images) in the document + has_image = False + for paragraph in doc.paragraphs: + if paragraph._element.xpath(".//pic:pic"): + has_image = True + break + # Alternative check: document should have at least one run with an inline shape + for paragraph in doc.paragraphs: + for run in paragraph.runs: + if hasattr(run._element, "xpath"): + pics = run._element.xpath(".//pic:pic") + if pics: + has_image = True + break + + assert has_image, "Document should contain an image" + + finally: + # Clean up temporary image + if os.path.exists(tmp_img_path): + os.unlink(tmp_img_path) + + def test_image_with_title(self): + """Test converting markdown with an image with a title.""" + # Create a temporary test image + with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_img: + img = Image.new("RGB", (100, 100), color="blue") + img.save(tmp_img.name) + tmp_img_path = tmp_img.name + + try: + # Prepare markdown with image reference and title + markdown = f'![alt text]({tmp_img_path} "Test Image Title")' + + # Execute + doc = markdown_to_word_object(markdown) + + # Verify + assert isinstance(doc, DocumentType) + + # Should have the image and a caption + has_image = False + has_caption = False + + for paragraph in doc.paragraphs: + # Check for image + if paragraph._element.xpath(".//pic:pic"): + has_image = True + # Check for caption + if ( + paragraph.style.name == "Caption" + and "Test Image Title" in paragraph.text + ): + has_caption = True + + # Alternative check for images + if not has_image: + for paragraph in doc.paragraphs: + for run in paragraph.runs: + if hasattr(run._element, "xpath"): + pics = run._element.xpath(".//pic:pic") + if pics: + has_image = True + break + + assert has_image, "Document should contain an image" + assert has_caption, "Document should contain a caption with the title" + + finally: + # Clean up temporary image + if os.path.exists(tmp_img_path): + os.unlink(tmp_img_path) + + def test_image_nonexistent_file(self): + """Test that nonexistent image files are handled gracefully.""" + # Prepare markdown with reference to nonexistent image + markdown = "![alt text](nonexistent_image.png)" + + # Execute + doc = markdown_to_word_object(markdown) + + # Verify - should not crash, just skip the image + assert isinstance(doc, DocumentType) + # Document should be created but without any images From a1b65200e9758979ef36a9652dbe14adaeb05af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Fri, 16 Jan 2026 19:28:47 +0100 Subject: [PATCH 02/11] Added example project with SDL --- examples/sdl-project/Makefile | 144 ++++++++++++++++++ examples/sdl-project/Makefile.modelcheck | 36 +++++ examples/sdl-project/deploymentview.dv.xml | 26 ++++ examples/sdl-project/deploymentview.ui.xml | 16 ++ examples/sdl-project/interfaceview.ui.xml | 40 +++++ examples/sdl-project/interfaceview.xml | 91 +++++++++++ examples/sdl-project/sdl-project.acn | 4 + examples/sdl-project/sdl-project.asn | 7 + examples/sdl-project/sdl-project.pro | 18 +++ .../work/counter/SDL/src/counter.pr | 52 +++++++ .../counter/SDL/src/counter_datamodel.asn | 20 +++ .../work/counter/SDL/src/dataview-uniq.asn | 53 +++++++ .../work/counter/SDL/src/system_structure.pr | 47 ++++++ .../implem/default/SDL/src/dataview-uniq.asn | 53 +++++++ .../harness/implem/default/SDL/src/harness.pr | 34 +++++ .../default/SDL/src/harness_datamodel.asn | 20 +++ .../default/SDL/src/system_structure.pr | 47 ++++++ examples/sdl-project/work/system.asn | 19 +++ 18 files changed, 727 insertions(+) create mode 100644 examples/sdl-project/Makefile create mode 100644 examples/sdl-project/Makefile.modelcheck create mode 100755 examples/sdl-project/deploymentview.dv.xml create mode 100644 examples/sdl-project/deploymentview.ui.xml create mode 100644 examples/sdl-project/interfaceview.ui.xml create mode 100644 examples/sdl-project/interfaceview.xml create mode 100644 examples/sdl-project/sdl-project.acn create mode 100644 examples/sdl-project/sdl-project.asn create mode 100644 examples/sdl-project/sdl-project.pro create mode 100644 examples/sdl-project/work/counter/SDL/src/counter.pr create mode 100644 examples/sdl-project/work/counter/SDL/src/counter_datamodel.asn create mode 100644 examples/sdl-project/work/counter/SDL/src/dataview-uniq.asn create mode 100644 examples/sdl-project/work/counter/SDL/src/system_structure.pr create mode 100644 examples/sdl-project/work/harness/implem/default/SDL/src/dataview-uniq.asn create mode 100644 examples/sdl-project/work/harness/implem/default/SDL/src/harness.pr create mode 100644 examples/sdl-project/work/harness/implem/default/SDL/src/harness_datamodel.asn create mode 100644 examples/sdl-project/work/harness/implem/default/SDL/src/system_structure.pr create mode 100644 examples/sdl-project/work/system.asn diff --git a/examples/sdl-project/Makefile b/examples/sdl-project/Makefile new file mode 100644 index 0000000..b1faea6 --- /dev/null +++ b/examples/sdl-project/Makefile @@ -0,0 +1,144 @@ +KAZOO?=kazoo +SPACECREATOR?=spacecreator.AppImage + +# Here you can specify custom compiler/linker flags, and add folders containing +# external code you want to compile and link for a specific partition. +# Use upper case for the partition name: +# +# export _USER_CFLAGS=... +# export _USER_LDFLAGS=... +# export _EXTERNAL_SOURCE_PATH=... +# +# NOTE: this can also be done in the Deployment View directly + +# If you need to reset this Makefile to its original state, run: +# $ taste reset + +# Disable the progress bar from taste-update-data-view when building systems +export NO_PROGRESS_BAR=1 + +# Get the list of ASN.1 files from Space Creator project file: +DISTFILES=$(shell qmake sdl-project.pro -o /tmp/null 2>&1) +# Exclude system.asn here as the types inside do not changes - only the values in the PID +# enumeration change, but this does not affect the content of DataView.aadl +ASN1_FILES=$(shell find ${DISTFILES} 2>/dev/null | egrep '\.asn$$|\.asn1$$' | grep -v system.asn) + +all: release + +include Makefile.modelcheck + +release: work/glue_release + rm -rf work/glue_debug + rm -rf work/glue_coverage + $(MAKE) -C work check_targets + $(MAKE) -C work + +debug: work/glue_debug + rm -rf work/glue_release + rm -rf work/glue_coverage + $(MAKE) -C work check_targets + $(MAKE) -C work + +coverage: work/glue_coverage + rm -rf work/glue_release + rm -rf work/glue_debug + $(MAKE) -C work check_targets + $(MAKE) -C work + +# To build and run the system type e.g. 'make debug run' +run: + $(MAKE) -C work run + +# To run Cheddar/Marzhin for scheduling analysis, type 'make edit_cv' +edit_cv: + $(MAKE) -C work run_cv + +# Simulation target (experimental - for systems made of SDL functions only) +simu: + if [ -f work/glue_debug ] || [ -f work/glue_release ] || [ -f work/glue_coverage ]; then $(MAKE) clean; fi + $(MAKE) interfaceview work/glue_simu + $(MAKE) -C work + $(MAKE) -C work/simulation -f Makefile.Simulation simu + +# Simulation and model checking: shortcut to create observer.asn +observer_dataview: + $(MAKE) DataView.aadl + $(MAKE) InterfaceView.aadl + $(MAKE) interfaceview + $(KAZOO) --glue -t SIMU + $(MAKE) -C work dataview/dataview-uniq.asn + $(MAKE) -C work/build -f Makefile.taste observer.asn + +skeletons: + $(MAKE) work/skeletons_built + +work/skeletons_built: InterfaceView.aadl DataView.aadl + $(KAZOO) --gw -o work + $(MAKE) -C work dataview simulink_skeletons + touch DataView.aadl # to avoid rebuilds due to new system.asn + touch $@ + +work/glue_simu: InterfaceView.aadl DataView.aadl + $(KAZOO) -t SIMU --glue --gw + $(MAKE) -C work dataview + touch DataView.aadl + touch $@ + +work/glue_release: InterfaceView.aadl DeploymentView.aadl DataView.aadl + sed -i 's/CoverageEnabled => true/CoverageEnabled => false/g' DeploymentView.aadl || : + $(KAZOO) -p --glue --gw -o work + touch DataView.aadl + touch $@ + +work/glue_debug: InterfaceView.aadl DeploymentView.aadl DataView.aadl + sed -i 's/CoverageEnabled => true/CoverageEnabled => false/g' DeploymentView.aadl || : + $(KAZOO) --debug -p --glue --gw -o work + touch DataView.aadl + touch $@ + +work/glue_coverage: InterfaceView.aadl DeploymentView.aadl DataView.aadl + sed -i 's/CoverageEnabled => false/CoverageEnabled => true/g' DeploymentView.aadl || : + $(KAZOO) --debug -p --glue --gw -o work + touch DataView.aadl + touch $@ + +InterfaceView.aadl: interfaceview.xml + $(SPACECREATOR) --aadlconverter -o $^ -t $(shell taste-config --prefix)/share/xml2aadl/interfaceview.tmplt -x $@ + +%: %.dv.xml Default_Deployment.aadl + # Build using deployment view $^ + @# We must update the .aadl only if the dv.xml file has changed (more recent timestamp) + if [ $< -nt $@.aadl ]; then $(SPACECREATOR) --dvconverter -o $< -t $(shell taste-config --prefix)/share/dv2aadl/deploymentview.tmplt -x $@.aadl; fi; + rsync --checksum $@.aadl DeploymentView.aadl + +interfaceview: Default_Deployment.aadl + # Build when no deployment view is open - use default + rsync --checksum $< DeploymentView.aadl + +Default_Deployment.aadl: interfaceview.xml + # Create/update a default deployment view for Linux target, if none other is provided + $(SPACECREATOR) --aadlconverter -o $^ -t $(shell taste-config --prefix)/share/xml2dv/interfaceview.tmplt -x $@ || exit 1 + rsync --checksum $@ DeploymentView.aadl + +DeploymentView.aadl: Default_Deployment.aadl + +DataView.aadl: ${ASN1_FILES} + $(info Generating/Updating DataView.aadl) + taste-update-data-view $^ work/system.asn + +clean: + rm -rf work/build work/dataview work/glue_simu + rm -f *.aadl # Interface and Deployment views in AADL are generated + rm -f work/glue_release work/glue_debug work/glue_coverage work/skeletons_built + find work -type d -name "wrappers" -exec rm -rf {} + || : + find work -type d -name "*_GUI" -exec rm -rf {} + || : + find work -type d -path "*/QGenC/xmi" -exec rm -rf {} + || : + find work -type d -path "*/QGenC/src/.qgeninfo" -exec rm -rf {} + || : + find work -type d -path "*/QGenC/src/slprj" -exec rm -rf {} + || : + find work -type f -path "*/QGenC/src/built" -exec rm -f {} + || : + find work -type f -path "*/QGenC/src/*.slxc" -exec rm -f {} + || : + find work -type f -path "*/QGenC/src/*.h" -not -name "simulink_definition_of_types.h" -not -name "*_invoke_ri.h" -exec rm -f {} + || : + find work -type f -path "*/QGenC/src/*.c" -exec rm -f {} + || : + +.PHONY: clean release debug coverage skeletons simu run simulink_skeletons + diff --git a/examples/sdl-project/Makefile.modelcheck b/examples/sdl-project/Makefile.modelcheck new file mode 100644 index 0000000..f07a8f9 --- /dev/null +++ b/examples/sdl-project/Makefile.modelcheck @@ -0,0 +1,36 @@ +model-check: InterfaceView.aadl DeploymentView.aadl DataView.aadl + $(KAZOO) -gw --glue -t MOCHECK + $(MAKE) -C work model-check + +create-obs: work/modelchecking/properties work/modelchecking observer_dataview + mkdir -p work/modelchecking/properties/$(NAME) + make -C work obs-skeleton NAME=$(NAME) + +create-msc: work/modelchecking/properties work/modelchecking observer_dataview + mkdir -p work/modelchecking/properties/$(NAME) + make -C work msc-skeleton NAME=$(NAME) + +create-bsc: work/modelchecking/properties work/modelchecking observer_dataview + mkdir -p work/modelchecking/properties/$(NAME) + make -C work bsc-skeleton NAME=$(NAME) + +work/modelchecking/properties: + mkdir -p work/modelchecking/properties + +work/modelchecking: + mkdir -p work/modelchecking + +create-subtype: work/modelchecking/subtypes work/modelchecking + find work/ -path work/binaries -prune -o -name subtype_*.asn -exec cat {} \; > work/modelchecking/subtypes/$(NAME).asn + +work/modelchecking/subtypes: + mkdir -p work/modelchecking/subtypes + +# Native model cheker target (experimental - for systems made of SDL functions only) +native_modelchecker: + if [ -f work/glue_debug ] || [ -f work/glue_release ] || [ -f work/glue_coverage ]; then $(MAKE) clean; fi + $(MAKE) interfaceview work/glue_simu + $(MAKE) -C work + $(MAKE) -C work/simulation -f Makefile.Simulation modelcheck + cd work/simulation && ./modelcheck + diff --git a/examples/sdl-project/deploymentview.dv.xml b/examples/sdl-project/deploymentview.dv.xml new file mode 100755 index 0000000..33f7a30 --- /dev/null +++ b/examples/sdl-project/deploymentview.dv.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/sdl-project/deploymentview.ui.xml b/examples/sdl-project/deploymentview.ui.xml new file mode 100644 index 0000000..67be8fe --- /dev/null +++ b/examples/sdl-project/deploymentview.ui.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/sdl-project/interfaceview.ui.xml b/examples/sdl-project/interfaceview.ui.xml new file mode 100644 index 0000000..ec3b51f --- /dev/null +++ b/examples/sdl-project/interfaceview.ui.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/sdl-project/interfaceview.xml b/examples/sdl-project/interfaceview.xml new file mode 100644 index 0000000..d42b148 --- /dev/null +++ b/examples/sdl-project/interfaceview.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/sdl-project/sdl-project.acn b/examples/sdl-project/sdl-project.acn new file mode 100644 index 0000000..e50982e --- /dev/null +++ b/examples/sdl-project/sdl-project.acn @@ -0,0 +1,4 @@ +SDL-PROJECT-DATAVIEW DEFINITIONS ::= BEGIN + +END + diff --git a/examples/sdl-project/sdl-project.asn b/examples/sdl-project/sdl-project.asn new file mode 100644 index 0000000..3100dda --- /dev/null +++ b/examples/sdl-project/sdl-project.asn @@ -0,0 +1,7 @@ +SDL-PROJECT-DATAVIEW DEFINITIONS ::= +BEGIN + + CounterValue ::= INTEGER (0 .. 10000) + +END + diff --git a/examples/sdl-project/sdl-project.pro b/examples/sdl-project/sdl-project.pro new file mode 100644 index 0000000..77445cd --- /dev/null +++ b/examples/sdl-project/sdl-project.pro @@ -0,0 +1,18 @@ +TEMPLATE = lib +CONFIG -= qt +CONFIG += generateC + +DISTFILES += $(HOME)/tool-inst/share/taste-types/taste-types.asn \ + deploymentview.dv.xml +DISTFILES += +DISTFILES += interfaceview.xml +DISTFILES += work/binaries/*.msc +DISTFILES += work/binaries/coverage/index.html +DISTFILES += work/binaries/filters +DISTFILES += work/system.asn + +DISTFILES += sdl-project.asn +DISTFILES += sdl-project.acn +include(work/taste.pro) +message($$DISTFILES) + diff --git a/examples/sdl-project/work/counter/SDL/src/counter.pr b/examples/sdl-project/work/counter/SDL/src/counter.pr new file mode 100644 index 0000000..475fd83 --- /dev/null +++ b/examples/sdl-project/work/counter/SDL/src/counter.pr @@ -0,0 +1,52 @@ +/* CIF PROCESS (250, 150), (150, 75) */ +/* CIF Keep Specific Geode _REQSERVER_ 'https://gitlab.esa.int/taste/demo' */ +process Counter; + /* CIF Keep Specific Geode Partition 'default' */ + /* CIF TEXT (191, 0), (170, 140) */ + DCL value CounterValue; + /* CIF ENDTEXT */ + /* CIF START (461, 85), (70, 35) */ + START; + /* CIF task (456, 140), (78, 35) */ + task value := 0; + /* CIF NEXTSTATE (461, 190), (70, 35) */ + NEXTSTATE Wait; + /* CIF state (533, 275), (70, 35) */ + /* CIF Keep Specific Geode Partition 'default' */ + state *; + /* CIF input (580, 330), (70, 35) */ + input do_get; + /* CIF output (554, 385), (122, 35) */ + output do_report(value); + /* CIF NEXTSTATE (580, 440), (70, 35) */ + NEXTSTATE -; + /* CIF input (466, 330), (75, 35) */ + input do_reset; + /* CIF task (464, 385), (78, 35) */ + task value := 0; + /* CIF NEXTSTATE (469, 435), (70, 35) */ + NEXTSTATE Wait; + endstate; + /* CIF state (769, 80), (70, 35) */ + /* CIF Keep Specific Geode Partition 'default' */ + state Count; + /* CIF input (831, 135), (70, 35) */ + input do_stop; + /* CIF NEXTSTATE (832, 190), (70, 35) */ + NEXTSTATE Wait; + /* CIF input (724, 135), (70, 35) */ + input tick; + /* CIF task (697, 190), (123, 35) */ + task value := value + 1; + /* CIF NEXTSTATE (724, 245), (70, 35) */ + NEXTSTATE Count; + endstate; + /* CIF state (589, 81), (70, 35) */ + /* CIF Keep Specific Geode Partition 'default' */ + state Wait; + /* CIF input (587, 136), (72, 35) */ + input do_start; + /* CIF NEXTSTATE (589, 191), (70, 35) */ + NEXTSTATE Count; + endstate; +endprocess Counter; \ No newline at end of file diff --git a/examples/sdl-project/work/counter/SDL/src/counter_datamodel.asn b/examples/sdl-project/work/counter/SDL/src/counter_datamodel.asn new file mode 100644 index 0000000..d4a2905 --- /dev/null +++ b/examples/sdl-project/work/counter/SDL/src/counter_datamodel.asn @@ -0,0 +1,20 @@ +Counter-Datamodel DEFINITIONS ::= +BEGIN +-- This file was generated automatically by OpenGEODE +IMPORTS + CounterValue FROM SDL-PROJECT-DATAVIEW + T-Int32, T-UInt32, T-Int8, T-UInt8, T-Boolean, T-Null-Record, T-Runtime-Error FROM TASTE-BasicTypes + PID-Range, PID FROM System-Dataview; + +Counter-States ::= ENUMERATED {count, wait} + +Counter-Context ::= SEQUENCE { + state Counter-States, + init-done BOOLEAN, + sender PID, + offspring PID, + value CounterValue +} + +Counter-T-Runtime-Error-Selection ::= ENUMERATED {noerror-present(1), encodeerror-present(2), decodeerror-present(3)} +END diff --git a/examples/sdl-project/work/counter/SDL/src/dataview-uniq.asn b/examples/sdl-project/work/counter/SDL/src/dataview-uniq.asn new file mode 100644 index 0000000..cf1ea5f --- /dev/null +++ b/examples/sdl-project/work/counter/SDL/src/dataview-uniq.asn @@ -0,0 +1,53 @@ +SDL-PROJECT-DATAVIEW DEFINITIONS ::= +BEGIN + + CounterValue ::= INTEGER (0 .. 10000) + +END + + +TASTE-BasicTypes DEFINITIONS ::= +BEGIN + +-- Set of TASTE predefined basic types + +T-Int32 ::= INTEGER (-2147483648 .. 2147483647) + +T-UInt32 ::= INTEGER (0 .. 4294967295) + +T-Int8 ::= INTEGER (-128 .. 127) + +T-UInt8 ::= INTEGER (0 .. 255) + +T-Boolean ::= BOOLEAN + +T-Null-Record ::= SEQUENCE {} + +T-Runtime-Error ::= CHOICE { + noerror T-UInt32, -- this shall be NULL, but DMT does not support NULL and SEDS does not support empty sequences + encodeerror T-Int32, -- the names shall be changed after fix in seds converter (space creator) will be merged + decodeerror T-Int32 +} + +END + +-- Dataview generated on-the-fly providing information on the system +-- and made available to the user code. +System-Dataview DEFINITIONS ::= +BEGIN + + -- Range of PID - type can be used to size arrays of PID type + PID-Range ::= INTEGER(0..2) + + -- List of functions (instances) present in the system + PID ::= ENUMERATED { + counter, + + harness, + + + env + } + +END + diff --git a/examples/sdl-project/work/counter/SDL/src/system_structure.pr b/examples/sdl-project/work/counter/SDL/src/system_structure.pr new file mode 100644 index 0000000..a62b0c7 --- /dev/null +++ b/examples/sdl-project/work/counter/SDL/src/system_structure.pr @@ -0,0 +1,47 @@ +-- Generated by TASTE (kazoo/templates/skeletons/opengeode-structure/function.tmplt) +-- DO NOT EDIT THIS FILE, IT WILL BE OVERWRITTEN DURING THE BUILD +/* CIF Keep Specific Geode ASNFilename 'dataview-uniq.asn' */ +use Datamodel; + +system Counter; + + signal do_get; + + + signal do_reset; + + + signal do_start; + + + signal do_stop; + + + signal tick; + + /* CIF Keep Specific Geode PARAMNAMES value */ + signal do_report (CounterValue); + + -- For internal use, return the PID of the caller + procedure get_sender; + fpar out sender PID; + external; + procedure get_last_error; + fpar out err T_Runtime_Error; + external; + + channel c + from env to Counter with do_get, do_reset, do_start, do_stop, tick; + from Counter to env with do_report; + endchannel; + + block Counter; + + signalroute r + from env to Counter with do_get, do_reset, do_start, do_stop, tick; + from Counter to env with do_report; + connect c and r; + + process Counter referenced; + endblock; +endsystem; diff --git a/examples/sdl-project/work/harness/implem/default/SDL/src/dataview-uniq.asn b/examples/sdl-project/work/harness/implem/default/SDL/src/dataview-uniq.asn new file mode 100644 index 0000000..cf1ea5f --- /dev/null +++ b/examples/sdl-project/work/harness/implem/default/SDL/src/dataview-uniq.asn @@ -0,0 +1,53 @@ +SDL-PROJECT-DATAVIEW DEFINITIONS ::= +BEGIN + + CounterValue ::= INTEGER (0 .. 10000) + +END + + +TASTE-BasicTypes DEFINITIONS ::= +BEGIN + +-- Set of TASTE predefined basic types + +T-Int32 ::= INTEGER (-2147483648 .. 2147483647) + +T-UInt32 ::= INTEGER (0 .. 4294967295) + +T-Int8 ::= INTEGER (-128 .. 127) + +T-UInt8 ::= INTEGER (0 .. 255) + +T-Boolean ::= BOOLEAN + +T-Null-Record ::= SEQUENCE {} + +T-Runtime-Error ::= CHOICE { + noerror T-UInt32, -- this shall be NULL, but DMT does not support NULL and SEDS does not support empty sequences + encodeerror T-Int32, -- the names shall be changed after fix in seds converter (space creator) will be merged + decodeerror T-Int32 +} + +END + +-- Dataview generated on-the-fly providing information on the system +-- and made available to the user code. +System-Dataview DEFINITIONS ::= +BEGIN + + -- Range of PID - type can be used to size arrays of PID type + PID-Range ::= INTEGER(0..2) + + -- List of functions (instances) present in the system + PID ::= ENUMERATED { + counter, + + harness, + + + env + } + +END + diff --git a/examples/sdl-project/work/harness/implem/default/SDL/src/harness.pr b/examples/sdl-project/work/harness/implem/default/SDL/src/harness.pr new file mode 100644 index 0000000..b165d41 --- /dev/null +++ b/examples/sdl-project/work/harness/implem/default/SDL/src/harness.pr @@ -0,0 +1,34 @@ +/* CIF PROCESS (250, 150), (150, 75) */ +/* CIF Keep Specific Geode _REQSERVER_ 'https://gitlab.esa.int/taste/demo' */ +process Harness; + /* CIF Keep Specific Geode Partition 'default' */ + /* CIF TEXT (699, 43), (170, 140) */ + DCL x CounterValue; + /* CIF ENDTEXT */ + /* CIF START (320, 10), (70, 35) */ + START; + /* CIF NEXTSTATE (320, 60), (70, 35) */ + NEXTSTATE Wait; + /* CIF state (450, 10), (70, 35) */ + /* CIF Keep Specific Geode Partition 'default' */ + state Wait; + /* CIF input (481, 65), (97, 35) */ + input do_report(x); + /* CIF PROCEDURECALL (488, 120), (82, 35) */ + call writeln(x); + /* CIF NEXTSTATE (495, 175), (70, 35) */ + NEXTSTATE wait; + /* CIF input (405, 65), (70, 35) */ + input trigger; + /* CIF output (402, 120), (75, 35) */ + output do_reset; + /* CIF output (403, 175), (72, 35) */ + output do_start; + /* CIF output (404, 230), (70, 35) */ + output do_stop; + /* CIF output (405, 285), (70, 35) */ + output do_get; + /* CIF NEXTSTATE (405, 340), (70, 35) */ + NEXTSTATE Wait; + endstate; +endprocess Harness; \ No newline at end of file diff --git a/examples/sdl-project/work/harness/implem/default/SDL/src/harness_datamodel.asn b/examples/sdl-project/work/harness/implem/default/SDL/src/harness_datamodel.asn new file mode 100644 index 0000000..55d8317 --- /dev/null +++ b/examples/sdl-project/work/harness/implem/default/SDL/src/harness_datamodel.asn @@ -0,0 +1,20 @@ +Harness-Datamodel DEFINITIONS ::= +BEGIN +-- This file was generated automatically by OpenGEODE +IMPORTS + CounterValue FROM SDL-PROJECT-DATAVIEW + T-Int32, T-UInt32, T-Int8, T-UInt8, T-Boolean, T-Null-Record, T-Runtime-Error FROM TASTE-BasicTypes + PID-Range, PID FROM System-Dataview; + +Harness-States ::= ENUMERATED {wait} + +Harness-Context ::= SEQUENCE { + state Harness-States, + init-done BOOLEAN, + sender PID, + offspring PID, + x CounterValue +} + +Harness-T-Runtime-Error-Selection ::= ENUMERATED {noerror-present(1), encodeerror-present(2), decodeerror-present(3)} +END diff --git a/examples/sdl-project/work/harness/implem/default/SDL/src/system_structure.pr b/examples/sdl-project/work/harness/implem/default/SDL/src/system_structure.pr new file mode 100644 index 0000000..30f87f8 --- /dev/null +++ b/examples/sdl-project/work/harness/implem/default/SDL/src/system_structure.pr @@ -0,0 +1,47 @@ +-- Generated by TASTE (kazoo/templates/skeletons/opengeode-structure/function.tmplt) +-- DO NOT EDIT THIS FILE, IT WILL BE OVERWRITTEN DURING THE BUILD +/* CIF Keep Specific Geode ASNFilename 'dataview-uniq.asn' */ +use Datamodel; + +system Harness; + + /* CIF Keep Specific Geode PARAMNAMES value */ + signal do_report (CounterValue); + + + signal trigger; + + signal do_get; + + + signal do_reset; + + + signal do_start; + + + signal do_stop; + + -- For internal use, return the PID of the caller + procedure get_sender; + fpar out sender PID; + external; + procedure get_last_error; + fpar out err T_Runtime_Error; + external; + + channel c + from env to Harness with do_report, trigger; + from Harness to env with do_get, do_reset, do_start, do_stop; + endchannel; + + block Harness; + + signalroute r + from env to Harness with do_report, trigger; + from Harness to env with do_get, do_reset, do_start, do_stop; + connect c and r; + + process Harness referenced; + endblock; +endsystem; diff --git a/examples/sdl-project/work/system.asn b/examples/sdl-project/work/system.asn new file mode 100644 index 0000000..7706fd1 --- /dev/null +++ b/examples/sdl-project/work/system.asn @@ -0,0 +1,19 @@ +-- Dataview generated on-the-fly providing information on the system +-- and made available to the user code. +System-Dataview DEFINITIONS ::= +BEGIN + + -- Range of PID - type can be used to size arrays of PID type + PID-Range ::= INTEGER(0..2) + + -- List of functions (instances) present in the system + PID ::= ENUMERATED { + counter, + + harness, + + + env + } + +END From 131f9af5ad8ba80810fdc910168a50fc265e2402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Fri, 16 Jan 2026 19:42:55 +0100 Subject: [PATCH 03/11] Test example for SDL behaviour generation (paths need fixing) --- ...ecss-e-st-40c_4_3_software_behaviour.tmplt | 152 ++++++++++++++++++ examples/generate_sdl_behaviour.sh | 30 ++++ 2 files changed, 182 insertions(+) create mode 100644 data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt create mode 100755 examples/generate_sdl_behaviour.sh diff --git a/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt b/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt new file mode 100644 index 0000000..c2ef1b8 --- /dev/null +++ b/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt @@ -0,0 +1,152 @@ +<% +## Data Initialization +# Get all functions + +funcs = [] +def get_function_children(func): + result = [] + if func.nested_functions: + for nested in func.nested_functions: + result.append(nested) + result.extend(get_function_children(nested)) + return result + +for func in interface_view.functions: + funcs.append(func) + funcs.extend(get_function_children(func)) + +funcs.sort(key=lambda f: f.name.lower()) + +# Get functions deployed to the target partition +target_partition_name = values["TARGET"] + +deployed_funcs = [] +target_partition = None +for node in deployment_view.nodes: + for partition in node.partitions: + if partition.name == target_partition_name: + target_partition = partition + +deployed_func_names = [f.name for f in target_partition.functions] +for fun in funcs: + if fun.name in deployed_func_names: + deployed_funcs.append(fun) + +# Only leaf functions are deployed, so a correction for parents must be applied +for func in funcs: + if func.nested_functions: + for nested in func.nested_functions: + if nested in deployed_funcs and not func in deployed_funcs: + deployed_funcs.append(func) + deployed_func_names.append(func.name) + +# Filter SDL functions only +import os +import subprocess +import glob + +sdl_funcs = [func for func in deployed_funcs if func.language and func.language.value == "SDL"] + +# Generate SDL behavior diagrams using OpenGEODE +def generate_sdl_images(func): + """Generate SDL images for a function using OpenGEODE""" + func_lower = func.name.lower() + images = [] + + # Try different path patterns for SDL/src directory + # Pattern 1: work/{function}/SDL/src + sdl_path_1 = f"work/{func_lower}/SDL/src" + # Pattern 2: work/{function}/implem/{implementation}/SDL/src + sdl_path_2_pattern = f"work/{func_lower}/implem/*/SDL/src" + + sdl_paths = [] + if os.path.exists(sdl_path_1): + sdl_paths.append(sdl_path_1) + else: + # Check for implementation-specific paths + matching_paths = glob.glob(sdl_path_2_pattern) + sdl_paths.extend(matching_paths) + + for sdl_path in sdl_paths: + if not os.path.exists(sdl_path): + continue + + # Find the system_structure.pr and function.pr files + system_pr = os.path.join(sdl_path, "system_structure.pr") + func_pr = os.path.join(sdl_path, f"{func_lower}.pr") + + if not os.path.exists(system_pr) or not os.path.exists(func_pr): + continue + + # Generate images using OpenGEODE + try: + # Change to SDL/src directory to run opengeode + original_dir = os.getcwd() + os.chdir(sdl_path) + + # Run OpenGEODE to generate PNG images + subprocess.run( + ["opengeode", "--png", "system_structure.pr", f"{func_lower}.pr"], + check=False, + capture_output=True + ) + + # Find all generated PNG files + png_files = glob.glob("*.png") + for png_file in sorted(png_files): + # Get relative path from current working directory + rel_path = os.path.relpath(os.path.join(sdl_path, png_file), original_dir) + # Extract caption from filename (remove extension) + caption = os.path.splitext(png_file)[0].replace("-", " ").replace("_", " ") + images.append((rel_path, caption)) + + os.chdir(original_dir) + except Exception as e: + # If OpenGEODE fails, just continue + if 'original_dir' in locals(): + os.chdir(original_dir) + pass + + return images + +# Generate images for all SDL functions +func_images = {} +for func in sdl_funcs: + images = generate_sdl_images(func) + if images: + func_images[func.name] = images + +%> +# Software Behaviour + +This section describes the behaviour of software components implemented in SDL. + +The behaviour of each SDL function is documented using state machine diagrams generated from the SDL implementation. + +% if not sdl_funcs: +*No SDL functions found in the ${target_partition_name} partition.* +% else: +## SDL Functions + +The following functions are implemented in SDL and their behaviour is documented below: + +% for func in sdl_funcs: +${"###"} ${func.name} + +**Description:** ${func.comment if func.comment else "No description available."} + +% if func.name in func_images and func_images[func.name]: +**Behavioural Diagrams:** + +The following diagrams illustrate the behaviour of the ${func.name} function: + +% for (img_path, caption) in func_images[func.name]: +![${func.name}](${img_path} "${caption}") + +% endfor +% else: +*No SDL diagrams available for this function. The function may not have SDL source files in the expected location, or OpenGEODE image generation was not successful.* +% endif + +% endfor +% endif diff --git a/examples/generate_sdl_behaviour.sh b/examples/generate_sdl_behaviour.sh new file mode 100755 index 0000000..65f798a --- /dev/null +++ b/examples/generate_sdl_behaviour.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +cd sdl-project +mkdir -p ../output + +# Generate MD behaviour documentation +template-processor --verbosity info \ + --value TARGET=ASW \ + --iv interfaceview.xml \ + --dv deploymentview.dv.xml \ + -o ../output \ + -t ../../data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt + +# Generate DOCX version +template-processor --verbosity info \ + --value TARGET=ASW \ + --iv interfaceview.xml \ + --dv deploymentview.dv.xml \ + -o ../output \ + -t ../../data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt \ + -p md2docx + +# Generate HTML version +template-processor --verbosity info \ + --value TARGET=ASW \ + --iv interfaceview.xml \ + --dv deploymentview.dv.xml \ + -o ../output \ + -t ../../data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt \ + -p md2html From bdfba5a3eb4904733a7fe5ba43a6644141a12acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Fri, 16 Jan 2026 22:34:24 +0100 Subject: [PATCH 04/11] Image files are copied to the output directory --- ...ecss-e-st-40c_4_3_software_behaviour.tmplt | 36 ++++++++++++++-- templateprocessor/cli.py | 9 +++- templateprocessor/md2docx.py | 41 ++++++++++++------- templateprocessor/postprocessor.py | 20 +++++---- templateprocessor/templateinstantiator.py | 4 ++ 5 files changed, 82 insertions(+), 28 deletions(-) diff --git a/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt b/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt index c2ef1b8..c230f41 100644 --- a/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt +++ b/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt @@ -82,6 +82,17 @@ def generate_sdl_images(func): try: # Change to SDL/src directory to run opengeode original_dir = os.getcwd() + + # Get absolute path to output directory before changing directories + abs_output_dir = None + if output_directory: + abs_output_dir = os.path.abspath(output_directory) + print(f"Output directory (absolute): {abs_output_dir}") + print(f"Output directory exists: {os.path.exists(abs_output_dir)}") + + # Get absolute path to SDL directory + abs_sdl_path = os.path.abspath(sdl_path) + os.chdir(sdl_path) # Run OpenGEODE to generate PNG images @@ -91,14 +102,31 @@ def generate_sdl_images(func): capture_output=True ) - # Find all generated PNG files + # Find all generated PNG files and move them to output directory png_files = glob.glob("*.png") + print(f"Found {len(png_files)} PNG files in {abs_sdl_path}") + for png_file in sorted(png_files): - # Get relative path from current working directory - rel_path = os.path.relpath(os.path.join(sdl_path, png_file), original_dir) # Extract caption from filename (remove extension) caption = os.path.splitext(png_file)[0].replace("-", " ").replace("_", " ") - images.append((rel_path, caption)) + + # If output_directory is specified, copy the image there + if abs_output_dir and os.path.exists(abs_output_dir): + import shutil + # Since we're in the sdl_path directory, png_file is in current dir + src_path = png_file + # Create unique filename with function name to avoid collisions + dest_filename = f"{func_lower}_{png_file}" + dest_path = os.path.join(abs_output_dir, dest_filename) + print(f"Copying {src_path} to {dest_path}") + shutil.copy2(src_path, dest_path) + # Use only the filename (no path) for markdown reference + images.append((dest_filename, caption)) + else: + # Fallback: use relative path from original working directory + abs_img_path = os.path.join(abs_sdl_path, png_file) + rel_path = os.path.relpath(abs_img_path, original_dir) + images.append((rel_path, caption)) os.chdir(original_dir) except Exception as e: diff --git a/templateprocessor/cli.py b/templateprocessor/cli.py index 4e0770d..d134ed2 100644 --- a/templateprocessor/cli.py +++ b/templateprocessor/cli.py @@ -2,6 +2,7 @@ Command Line Interface for Template Processor """ +import os import logging import argparse from pathlib import Path @@ -178,7 +179,11 @@ def instantiate( logging.debug(f"Instantiation:\n {instantiated_template}") output = str(Path(output_directory) / f"{name}") logging.debug(f"Postprocessing with {postprocessor_type}") - postprocessor.process(postprocessor_type, instantiated_template, output) + base_path = os.getcwd() + logging.debug(f"Base path set to {base_path}") + postprocessor.process( + postprocessor_type, instantiated_template, output, base_path + ) except FileNotFoundError as e: logging.error(f"File not found: {e.filename}") except Exception as e: @@ -216,7 +221,7 @@ def main(): values = get_values_dictionary(args.value) logging.info(f"Instantiating the TemplateInstantiator") - instantiator = TemplateInstantiator(iv, dv, sots, values) + instantiator = TemplateInstantiator(iv, dv, sots, values, args.output) logging.info(f"Instantiating the Postprocessor") postprocessor = Postprocessor( diff --git a/templateprocessor/md2docx.py b/templateprocessor/md2docx.py index 472300f..f78c5de 100644 --- a/templateprocessor/md2docx.py +++ b/templateprocessor/md2docx.py @@ -54,7 +54,8 @@ def process_list_items(list_element: Tag, doc: Document, style_base: str, level= if nested_ol: process_list_items(nested_ol, doc, "List Number", level + 1) -def embed_image(img : Tag, doc: Document): + +def embed_image(img: Tag, doc: Document, base_path: str = ""): img_src = img.get("src") img_title = img.get("title", "").strip() img_alt = img.get("alt", "").strip() @@ -62,21 +63,31 @@ def embed_image(img : Tag, doc: Document): # Use title if available, otherwise use alt text caption_text = img_title if img_title else img_alt - if img_src and os.path.exists(img_src): - try: - doc.add_picture(img_src, width=Inches(IMAGE_WIDTH_IN_INCHES)) - if caption_text: - caption_paragraph = doc.add_paragraph(caption_text) - caption_paragraph.style = "Caption" - except Exception: - # If image cannot be added, skip it silently - pass - -def markdown_to_word_file(markdown_source: str, word_file_path: str): - doc = markdown_to_word_object(markdown_source) + if img_src: + # Try the image path as-is first, then relative to base_path + image_path = img_src + if not os.path.exists(image_path) and base_path: + image_path = os.path.join(base_path, img_src) + + if os.path.exists(image_path): + try: + doc.add_picture(image_path, width=Inches(IMAGE_WIDTH_IN_INCHES)) + if caption_text: + caption_paragraph = doc.add_paragraph(caption_text) + caption_paragraph.style = "Caption" + except Exception: + # If image cannot be added, skip it silently + pass + + +def markdown_to_word_file( + markdown_source: str, word_file_path: str, base_path: str = "" +): + doc = markdown_to_word_object(markdown_source, base_path) doc.save(word_file_path) -def markdown_to_word_object(markdown_source: str) -> Document: + +def markdown_to_word_object(markdown_source: str, base_path: str = "") -> Document: # Converting Markdown to HTML html_content = markdown2.markdown(markdown_source, extras=["tables", "wiki-tables"]) @@ -98,7 +109,7 @@ def markdown_to_word_object(markdown_source: str) -> Document: # Check if paragraph contains an image img = element.find("img") if img: - embed_image(img, doc) + embed_image(img, doc, base_path) else: # Regular paragraph without image paragraph = doc.add_paragraph() diff --git a/templateprocessor/postprocessor.py b/templateprocessor/postprocessor.py index f2e7335..eff97e9 100644 --- a/templateprocessor/postprocessor.py +++ b/templateprocessor/postprocessor.py @@ -19,25 +19,26 @@ class PostprocessorType(Enum): class AbstractPostprocessor(ABC): @abstractmethod - def process(self, text: str, base_file_name: str) -> None: + def process(self, text: str, base_file_name: str, base_path: str = "") -> None: """ Process the input text and write to output file. Args: text: Input text string to process base_file_name: Path to output file, without extension + base_path: Base path for resolving relative image paths """ pass class Md2docxPostprocessor(AbstractPostprocessor): - def process(self, text: str, base_file_name: str) -> None: + def process(self, text: str, base_file_name: str, base_path: str = "") -> None: output_file_name = f"{base_file_name}.docx" - md2docx.markdown_to_word_file(text, output_file_name) + md2docx.markdown_to_word_file(text, output_file_name, base_path) class Md2HtmlPostprocessor(AbstractPostprocessor): - def process(self, text: str, base_file_name: str) -> None: + def process(self, text: str, base_file_name: str, base_path: str = "") -> None: output_file_name = f"{base_file_name}.html" html_content = markdown2.markdown(text, extras=["tables", "wiki-tables"]) with open(output_file_name, "w") as f: @@ -45,7 +46,7 @@ def process(self, text: str, base_file_name: str) -> None: class PassthroughPostprocessor(AbstractPostprocessor): - def process(self, text: str, base_file_name: str) -> None: + def process(self, text: str, base_file_name: str, base_path: str = "") -> None: output_file_name = f"{base_file_name}.md" with open(output_file_name, "w") as f: f.write(text) @@ -58,7 +59,11 @@ def __init__(self, registry: Dict[PostprocessorType, AbstractPostprocessor]): self.registry = registry def process( - self, postprocessor_type: PostprocessorType, text: str, base_file_name: str + self, + postprocessor_type: PostprocessorType, + text: str, + base_file_name: str, + base_path: str = "", ) -> None: """ Process the input text and write to output file based on processor type. @@ -67,7 +72,8 @@ def process( postprocessor_type: Desired postprocessor type text: Input text string to process base_file_name: Path to output file, without extension + base_path: Base path for resolving relative image paths """ if postprocessor_type not in self.registry: raise ValueError(f"Not supported postprocessor {postprocessor_type.value}") - self.registry[postprocessor_type].process(text, base_file_name) + self.registry[postprocessor_type].process(text, base_file_name, base_path) diff --git a/templateprocessor/templateinstantiator.py b/templateprocessor/templateinstantiator.py index 25f0c62..3ab6d4d 100644 --- a/templateprocessor/templateinstantiator.py +++ b/templateprocessor/templateinstantiator.py @@ -20,6 +20,7 @@ class TemplateInstantiator: values: Dict[str, str] interface_view: InterfaceView deployment_view: DeploymentView + output_directory: str def __init__( self, @@ -27,11 +28,13 @@ def __init__( deployment_view: DeploymentView, system_object_types: Dict[str, SystemObjectType], values: Dict[str, str], + output_directory: str = "", ): self.system_object_types = system_object_types self.interface_view = interface_view self.deployment_view = deployment_view self.values = values + self.output_directory = output_directory def instantiate(self, template: str, context_directory: str) -> str: mako_template = Template(text=template, module_directory=context_directory) @@ -41,6 +44,7 @@ def instantiate(self, template: str, context_directory: str) -> str: "interface_view": self.interface_view, "deployment_view": self.deployment_view, "values": self.values, + "output_directory": self.output_directory, } instantiated_text = str(mako_template.render(**context)) From 4169a2c4d360978225def96898e12b1253655dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Fri, 16 Jan 2026 22:41:13 +0100 Subject: [PATCH 05/11] Fixed paths for docx generation --- templateprocessor/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templateprocessor/cli.py b/templateprocessor/cli.py index d134ed2..f260023 100644 --- a/templateprocessor/cli.py +++ b/templateprocessor/cli.py @@ -179,10 +179,10 @@ def instantiate( logging.debug(f"Instantiation:\n {instantiated_template}") output = str(Path(output_directory) / f"{name}") logging.debug(f"Postprocessing with {postprocessor_type}") - base_path = os.getcwd() - logging.debug(f"Base path set to {base_path}") + # base_path = os.getcwd() + # logging.debug(f"Base path set to {base_path}") postprocessor.process( - postprocessor_type, instantiated_template, output, base_path + postprocessor_type, instantiated_template, output, output_directory ) except FileNotFoundError as e: logging.error(f"File not found: {e.filename}") From 6793e626023a9fa59530de35e3d0f064a72e827a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Fri, 16 Jan 2026 22:50:22 +0100 Subject: [PATCH 06/11] Updated README --- README.md | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 37c4e42..4831565 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,10 @@ TP is a template processing engine developed for TASTE Document Generator. Its m ## Installation -TODO +This project uses Python. The recommended way to install dependencies is via the provided Makefile which exposes a convenient `make install` target. + +Prerequisites: +- Python 3.10+ (or a compatible system Python) ## Configuration @@ -16,9 +19,41 @@ None ## Running -The assumed use case is for the Template Processor to be invoked by TASTE Document Generator. However, if TP is to be used manually, the following command line interface, as documented in the built-in help, is available: +The Template Processor can be run from the command line. The application name is `template-processor` which exposes the following arguments. Run the built-in help to see the same list: + +```bash +template-processor --help +``` + +Key command-line arguments: + +- `-i, --iv` : Input Interface View file (XML) +- `-d, --dv` : Input Deployment View file (XML) +- `-s, --system-objects` : One or more CSV files describing System Object Types (can be supplied multiple times) +- `-v, --value` : One or more name=value pairs to provide template values (e.g., `-v TARGET=ASW`) +- `-t, --template` : One or more template files to process (Mako templates). This argument can be provided multiple times. +- `-m, --module-directory` : Module directory for Mako to use for compiled template modules (optional) +- `-o, --output` : Output directory for processed templates (required) +- `--verbosity` : Logging verbosity (choices `info`, `debug`, `warning`, `error`, default `warning`) +- `-p, --postprocess` : Postprocessing option (choices `none`, `md2docx`, `md2html`; default `none`) + +Example usage: + +```bash +# instantiate a template and postprocess to DOCX +template-processor \\ + -i examples/demo-project/interfaceview.xml \\ + -d examples/demo-project/deploymentview.dv.xml \\ + -s data/parameters.csv \\ + -v TARGET=ASW \\ + -t data/ecss-template/ecss-e-st-40c_4_1_software_static_architecture.tmplt \\ + -o output \\ + -p md2docx +``` -TODO +Notes: +- The `-o/--output` directory will be used for writing generated files; templates may also copy or move generated assets (images) into that directory if supported by the template. +- When using `md2docx` postprocessing, image paths inside the generated Markdown should be resolvable from the working directory or the output directory so images are embedded correctly in the produced DOCX. ## Frequently Asked Questions (FAQ) From de29421cc255f0a14e43e673e32d2330cc71bee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Fri, 16 Jan 2026 22:50:53 +0100 Subject: [PATCH 07/11] Code cleanup --- templateprocessor/cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/templateprocessor/cli.py b/templateprocessor/cli.py index f260023..908bbdc 100644 --- a/templateprocessor/cli.py +++ b/templateprocessor/cli.py @@ -2,7 +2,6 @@ Command Line Interface for Template Processor """ -import os import logging import argparse from pathlib import Path @@ -179,8 +178,7 @@ def instantiate( logging.debug(f"Instantiation:\n {instantiated_template}") output = str(Path(output_directory) / f"{name}") logging.debug(f"Postprocessing with {postprocessor_type}") - # base_path = os.getcwd() - # logging.debug(f"Base path set to {base_path}") + # Base directory for postprocessing is the output directory postprocessor.process( postprocessor_type, instantiated_template, output, output_directory ) From 6625624d3e9094bac5693c33eb35846a5f99aafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Fri, 16 Jan 2026 22:59:41 +0100 Subject: [PATCH 08/11] Improved loggign --- templateprocessor/md2docx.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templateprocessor/md2docx.py b/templateprocessor/md2docx.py index f78c5de..20f5347 100644 --- a/templateprocessor/md2docx.py +++ b/templateprocessor/md2docx.py @@ -14,6 +14,7 @@ """ import markdown2 +import logging import os from docx import Document from docx.shared import Inches @@ -75,8 +76,8 @@ def embed_image(img: Tag, doc: Document, base_path: str = ""): if caption_text: caption_paragraph = doc.add_paragraph(caption_text) caption_paragraph.style = "Caption" - except Exception: - # If image cannot be added, skip it silently + except Exception as e: + logging.error(f"Exception while adding image {e}") pass From c434ff0dd8b9b4b8007eed32ee8af21527ab312d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Fri, 16 Jan 2026 23:06:34 +0100 Subject: [PATCH 09/11] Added pillow dependency --- requirements.txt | 3 ++- setup.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 06fb71a..c142e7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ black==24.3.0 mako==1.3.10 python-docx==1.2.0 bs4==0.0.2 -markdown2==2.5.4 \ No newline at end of file +markdown2==2.5.4 +pillow==11.1.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 1359fc0..0cbec93 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ 'flake8>=6.0.0', 'black>=23.0.0', 'mypy>=1.0.0', + 'pillow>=11.1.0' ], }, entry_points={ From 6652d2bfdbd096b8ab417992c85b34f365149721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Mon, 19 Jan 2026 13:28:48 +0100 Subject: [PATCH 10/11] Moved harness sources --- .../work/harness/{implem/default => }/SDL/src/dataview-uniq.asn | 0 .../work/harness/{implem/default => }/SDL/src/harness.pr | 0 .../harness/{implem/default => }/SDL/src/harness_datamodel.asn | 0 .../work/harness/{implem/default => }/SDL/src/system_structure.pr | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename examples/sdl-project/work/harness/{implem/default => }/SDL/src/dataview-uniq.asn (100%) rename examples/sdl-project/work/harness/{implem/default => }/SDL/src/harness.pr (100%) rename examples/sdl-project/work/harness/{implem/default => }/SDL/src/harness_datamodel.asn (100%) rename examples/sdl-project/work/harness/{implem/default => }/SDL/src/system_structure.pr (100%) diff --git a/examples/sdl-project/work/harness/implem/default/SDL/src/dataview-uniq.asn b/examples/sdl-project/work/harness/SDL/src/dataview-uniq.asn similarity index 100% rename from examples/sdl-project/work/harness/implem/default/SDL/src/dataview-uniq.asn rename to examples/sdl-project/work/harness/SDL/src/dataview-uniq.asn diff --git a/examples/sdl-project/work/harness/implem/default/SDL/src/harness.pr b/examples/sdl-project/work/harness/SDL/src/harness.pr similarity index 100% rename from examples/sdl-project/work/harness/implem/default/SDL/src/harness.pr rename to examples/sdl-project/work/harness/SDL/src/harness.pr diff --git a/examples/sdl-project/work/harness/implem/default/SDL/src/harness_datamodel.asn b/examples/sdl-project/work/harness/SDL/src/harness_datamodel.asn similarity index 100% rename from examples/sdl-project/work/harness/implem/default/SDL/src/harness_datamodel.asn rename to examples/sdl-project/work/harness/SDL/src/harness_datamodel.asn diff --git a/examples/sdl-project/work/harness/implem/default/SDL/src/system_structure.pr b/examples/sdl-project/work/harness/SDL/src/system_structure.pr similarity index 100% rename from examples/sdl-project/work/harness/implem/default/SDL/src/system_structure.pr rename to examples/sdl-project/work/harness/SDL/src/system_structure.pr From 8874845fd28bbd6d1a389d467f0599a5cea7a302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Mon, 19 Jan 2026 17:59:29 +0100 Subject: [PATCH 11/11] Added better logging to template --- ...ecss-e-st-40c_4_3_software_behaviour.tmplt | 1 - ...st-40c_5_5_internal_interface_design.tmplt | 105 +++++++++++++++--- 2 files changed, 91 insertions(+), 15 deletions(-) diff --git a/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt b/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt index c230f41..7e8d1b9 100644 --- a/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt +++ b/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt @@ -145,7 +145,6 @@ for func in sdl_funcs: func_images[func.name] = images %> -# Software Behaviour This section describes the behaviour of software components implemented in SDL. diff --git a/data/ecss-template/ecss-e-st-40c_5_5_internal_interface_design.tmplt b/data/ecss-template/ecss-e-st-40c_5_5_internal_interface_design.tmplt index dd2ecac..4467a50 100644 --- a/data/ecss-template/ecss-e-st-40c_5_5_internal_interface_design.tmplt +++ b/data/ecss-template/ecss-e-st-40c_5_5_internal_interface_design.tmplt @@ -4,7 +4,7 @@ def find_by_name(all, name): for f in all: - if f.name == name: + if f is not None and hasattr(f, 'name') and f.name == name: return f return None @@ -18,10 +18,11 @@ def get_function_children(func): return result for func in interface_view.functions: - funcs.append(func) - funcs.extend(get_function_children(func)) + if func is not None: + funcs.append(func) + funcs.extend(get_function_children(func)) -funcs.sort(key=lambda f: f.name.lower()) +funcs.sort(key=lambda f: f.name.lower() if f is not None and hasattr(f, 'name') else '') # Get functions deployed to the target partition target_partition_name = values["TARGET"] @@ -30,21 +31,28 @@ deployed_funcs = [] target_partition = None for node in deployment_view.nodes: for partition in node.partitions: - if partition.name == target_partition_name: + if partition is not None and hasattr(partition, 'name') and partition.name == target_partition_name: target_partition = partition - -deployed_func_names = [f.name for f in target_partition.functions] + +if target_partition is None: + print(f"WARNING: Target partition '{target_partition_name}' not found") + deployed_func_names = [] +else: + deployed_func_names = [f.name for f in target_partition.functions if f is not None and hasattr(f, 'name')] for fun in funcs: - if fun.name in deployed_func_names: + if fun is not None and hasattr(fun, 'name') and fun.name in deployed_func_names: deployed_funcs.append(fun) # Only leaf functions are deployed, so a correction for parents must be applied for func in funcs: - if func.nested_functions: + if func is not None and func.nested_functions: for nested in func.nested_functions: if nested in deployed_funcs and not func in deployed_funcs: deployed_funcs.append(func) - deployed_func_names.append(func.name) + if hasattr(func, 'name'): + deployed_func_names.append(func.name) + else: + print(f"WARNING: Function object has no 'name' attribute") @@ -66,9 +74,14 @@ for func in interface_view.functions: internal_connections = [] for connection in connections: - if connection.source is None or connection.source.function_name is None: + if connection is None: + print(f"WARNING: Null connection object found") continue - if connection.target is None or connection.target.function_name is None: + if connection.source is None or not hasattr(connection.source, 'function_name') or connection.source.function_name is None: + print(f"WARNING: Connection with invalid source found") + continue + if connection.target is None or not hasattr(connection.target, 'function_name') or connection.target.function_name is None: + print(f"WARNING: Connection with invalid target found") continue if connection.target.function_name in deployed_func_names and connection.source.function_name in deployed_func_names: internal_connections.append(connection) @@ -81,14 +94,32 @@ for connection in internal_connections: meta["connection"] = connection meta["source_function"] = find_by_name(funcs, connection.source.function_name) if meta["source_function"] is None: - print(f"Source function {connection.source.function_name} not found") + print(f"WARNING: Source function {connection.source.function_name} not found") continue meta["source_iface"] = find_by_name(meta["source_function"].provided_interfaces + meta["source_function"].required_interfaces, connection.source.iface_name) + if meta["source_iface"] is None: + print(f"WARNING: Source interface {connection.source.iface_name} not found in function {meta['source_function'].name}") + continue meta["target_function"] = find_by_name(funcs, connection.target.function_name) if meta["target_function"] is None: - print(f"Target function {connection.target.function_name} not found") + print(f"WARNING: Target function {connection.target.function_name} not found") continue meta["target_iface"] = find_by_name(meta["target_function"].provided_interfaces + meta["target_function"].required_interfaces, connection.source.iface_name) + if meta["target_iface"] is None: + print(f"WARNING: Target interface {connection.source.iface_name} not found in function {meta['target_function'].name}") + continue + if not hasattr(meta["source_function"], 'name'): + print(f"WARNING: Source function object has no 'name' attribute") + continue + if not hasattr(meta["source_iface"], 'name'): + print(f"WARNING: Source interface object has no 'name' attribute") + continue + if not hasattr(meta["target_function"], 'name'): + print(f"WARNING: Target function object has no 'name' attribute") + continue + if not hasattr(meta["target_iface"], 'name'): + print(f"WARNING: Target interface object has no 'name' attribute") + continue source_handle = f"{meta["source_function"].name}__{meta["source_iface"].name}" target_handle = f"{meta["target_function"].name}__{meta["target_iface"].name}" if not source_handle in iface_source_map.keys(): @@ -103,8 +134,22 @@ for connection in internal_connections: The list below summarizes all internal interfaces between user components. % for func in deployed_funcs: +% if func is None: +<% print(f"WARNING: Null function in deployed_funcs") %> +<% continue %> +% endif % for iface in func.provided_interfaces + func.required_interfaces: +% if iface is None: +<% print(f"WARNING: Null interface in function {func.name if hasattr(func, 'name') else 'unknown'}") %> +<% continue %> +% endif <% +if not hasattr(func, 'name'): + print(f"WARNING: Function object has no 'name' attribute") + continue +if not hasattr(iface, 'name'): + print(f"WARNING: Interface object has no 'name' attribute in function {func.name}") + continue iface_handle = f"{func.name}__{iface.name}" if not iface_handle in iface_source_map and not iface_handle in iface_target_map: continue @@ -123,6 +168,14 @@ ${"#"} \ % if iface.kind.value != "Cyclic": - Parameters: % for param in iface.input_parameters + iface.output_parameters: +% if param is None: +<% print(f"WARNING: Null parameter in interface {iface.name if hasattr(iface, 'name') else 'unknown'}") %> +<% continue %> +% endif +% if not hasattr(param, 'name'): +<% print(f"WARNING: Parameter object has no 'name' attribute in interface {iface.name if hasattr(iface, 'name') else 'unknown'}") %> +<% continue %> +% endif - \ % if param in iface.input_parameters: @@ -137,6 +190,18 @@ ${param.name} ${param.type} (with ${param.encoding.value} encoding) % if iface_handle in iface_source_map: - Connects to: % for (other_function, other_iface) in iface_source_map[iface_handle]: +% if other_function is None or other_iface is None: +<% print(f"WARNING: Null function or interface in source map for {iface_handle}") %> +<% continue %> +% endif +% if not hasattr(other_function, 'name'): +<% print(f"WARNING: Other function has no 'name' attribute in source map for {iface_handle}") %> +<% continue %> +% endif +% if not hasattr(other_iface, 'name'): +<% print(f"WARNING: Other interface has no 'name' attribute in source map for {iface_handle}") %> +<% continue %> +% endif - ${other_function.name}::${other_iface.name} % endfor @@ -145,6 +210,18 @@ ${param.name} ${param.type} (with ${param.encoding.value} encoding) % if iface_handle in iface_target_map: - Is connected from: % for (other_function, other_iface) in iface_target_map[iface_handle]: +% if other_function is None or other_iface is None: +<% print(f"WARNING: Null function or interface in target map for {iface_handle}") %> +<% continue %> +% endif +% if not hasattr(other_function, 'name'): +<% print(f"WARNING: Other function has no 'name' attribute in target map for {iface_handle}") %> +<% continue %> +% endif +% if not hasattr(other_iface, 'name'): +<% print(f"WARNING: Other interface has no 'name' attribute in target map for {iface_handle}") %> +<% continue %> +% endif - ${other_function.name}::${other_iface.name} % endfor