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) 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..7e8d1b9 --- /dev/null +++ b/data/ecss-template/ecss-e-st-40c_4_3_software_behaviour.tmplt @@ -0,0 +1,179 @@ +<% +## 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() + + # 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 + subprocess.run( + ["opengeode", "--png", "system_structure.pr", f"{func_lower}.pr"], + check=False, + capture_output=True + ) + + # 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): + # Extract caption from filename (remove extension) + caption = os.path.splitext(png_file)[0].replace("-", " ").replace("_", " ") + + # 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: + # 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 + +%> + +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/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 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/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 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/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/SDL/src/dataview-uniq.asn b/examples/sdl-project/work/harness/SDL/src/dataview-uniq.asn new file mode 100644 index 0000000..cf1ea5f --- /dev/null +++ b/examples/sdl-project/work/harness/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/SDL/src/harness.pr b/examples/sdl-project/work/harness/SDL/src/harness.pr new file mode 100644 index 0000000..b165d41 --- /dev/null +++ b/examples/sdl-project/work/harness/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/SDL/src/harness_datamodel.asn b/examples/sdl-project/work/harness/SDL/src/harness_datamodel.asn new file mode 100644 index 0000000..55d8317 --- /dev/null +++ b/examples/sdl-project/work/harness/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/SDL/src/system_structure.pr b/examples/sdl-project/work/harness/SDL/src/system_structure.pr new file mode 100644 index 0000000..30f87f8 --- /dev/null +++ b/examples/sdl-project/work/harness/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 diff --git a/examples/test_image1.png b/examples/test_image1.png new file mode 100644 index 0000000..4b7e23d Binary files /dev/null and b/examples/test_image1.png differ diff --git a/examples/test_image2.jpg b/examples/test_image2.jpg new file mode 100644 index 0000000..5e39535 Binary files /dev/null and b/examples/test_image2.jpg differ 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={ diff --git a/templateprocessor/cli.py b/templateprocessor/cli.py index 4e0770d..908bbdc 100644 --- a/templateprocessor/cli.py +++ b/templateprocessor/cli.py @@ -178,7 +178,10 @@ 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 directory for postprocessing is the output directory + postprocessor.process( + postprocessor_type, instantiated_template, output, output_directory + ) except FileNotFoundError as e: logging.error(f"File not found: {e.filename}") except Exception as e: @@ -216,7 +219,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 56588b1..20f5347 100644 --- a/templateprocessor/md2docx.py +++ b/templateprocessor/md2docx.py @@ -14,9 +14,14 @@ """ import markdown2 +import logging +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"): @@ -51,12 +56,39 @@ def process_list_items(list_element: Tag, doc: Document, style_base: str, level= process_list_items(nested_ol, doc, "List Number", level + 1) -def markdown_to_word_file(markdown_source: str, word_file_path: str): - doc = markdown_to_word_object(markdown_source) +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() + + # Use title if available, otherwise use alt text + caption_text = img_title if img_title else img_alt + + 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 as e: + logging.error(f"Exception while adding image {e}") + 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"]) @@ -75,14 +107,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, base_path) + 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..eff97e9 100644 --- a/templateprocessor/postprocessor.py +++ b/templateprocessor/postprocessor.py @@ -18,29 +18,27 @@ 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: @@ -48,8 +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) @@ -62,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. @@ -71,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)) 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