diff --git a/AddressSpace/templates/designToAddressSpaceDocMd.jinja b/AddressSpace/templates/designToAddressSpaceDocMd.jinja
new file mode 100755
index 00000000..1b17dfc4
--- /dev/null
+++ b/AddressSpace/templates/designToAddressSpaceDocMd.jinja
@@ -0,0 +1,127 @@
+{# © Copyright CERN, 2015. #}
+{# All rights not expressly granted are reserved. #}
+{# This file is part of Quasar. #}
+{# #}
+{# Quasar is free software: you can redistribute it and/or modify #}
+{# it under the terms of the GNU Lesser General Public Licence as published by #}
+{# the Free Software Foundation, either version 3 of the Licence. #}
+{# Quasar is distributed in the hope that it will be useful, #}
+{# but WITHOUT ANY WARRANTY; without even the implied warranty of #}
+{# #}
+{# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #}
+{# GNU Lesser General Public Licence for more details. #}
+{# #}
+{# You should have received a copy of the GNU Lesser General Public License #}
+{# along with Quasar. If not, see #}
+{# #}
+{# Created: Sep 2018 (the original XSLT version) #}
+{# 11 May 2020 (translated to Jinja2) #}
+{# 05 February 2024 (converted to Markdown) #}
+{# Authors: #}
+{# Piotr Nikiel #}
+
+{% macro estimateFunctionReadWrite(variable) -%}
+ {%- if 'cachevariable' in variable.tag -%}
+ {%- if variable.get('addressSpaceWrite') == 'forbidden' -%}
+ read-only
+ {%- else -%}
+ read+write
+ {%- endif -%}
+ {% else %}
+ {%- if variable.get('addressSpaceWrite') == 'forbidden' and variable.get('addressSpaceRead') == 'forbidden' -%}
+ none
+ {%- elif variable.get('addressSpaceWrite') == 'forbidden' and variable.get('addressSpaceRead') != 'forbidden' -%}
+ read-only
+ {%- elif variable.get('addressSpaceWrite') != 'forbidden' and variable.get('addressSpaceRead') == 'forbidden' -%}
+ write-only
+ {%- elif variable.get('addressSpaceWrite') != 'forbidden' and variable.get('addressSpaceRead') != 'forbidden' -%}
+ read+write
+ {%- endif -%}
+ {%- endif -%}
+{% endmacro %}
+
+# OPC-UA address space documentation for {{designInspector.getProjectName()}}
+{#
+Jump to:
+{% for class_name in designInspector.get_names_of_all_classes()|sort %}
+{% set cls = designInspector.objectify_class(class_name) %}
+ {{cls.get('name')}}
+{% endfor %}
+#}
+{% for class_name in designInspector.get_names_of_all_classes()|sort %}
+
+---
+{% set cls = designInspector.objectify_class(class_name) %}
+
+## {{cls.get('name')}}
+{% if (cls.documentation|length)>0 %}
+DOC:{{cls.documentation|node_text_contents_to_string|trim}}
+{% else %}
+DOC:
+{% endif %}
+
+{% if (cls.cachevariable|length + cls.sourcevariable|length + cls.method|length)>0 %}
+{%- if (cls.cachevariable|length + cls.sourcevariable|length)>0 -%}
+Variables:
+{# we combine cache-vars and source-vars because the end-user should see both alphabetically
+ without paying attention whether these are CV or SV. #}
+{% set variables = designInspector.to_list_if_exists(cls.cachevariable) + designInspector.to_list_if_exists(cls.sourcevariable) %}
+{% for variable in variables|sorted_by_objectified_attr('name') %}
+??? info "{{-variable.get('name')}} (dataType={{variable.get('dataType')}})"
+ Type:
+{%- if 'cachevariable' in variable.tag %}
+ cache-variable
+{% else %}
+ source-variable
+{%- endif %}
+ Access=
+{{-estimateFunctionReadWrite(variable)-}}
+{% if variable.documentation|length>0 %}
+
+ DOC:{{- variable.documentation|node_text_contents_to_string|trim }}
+
+{% endif %}
+{% endfor %}
+{% endif %}
+
+{% if cls.method|length>0 %}
+Methods:
+{%- for m in cls.method|sorted_by_objectified_attr('name') %}
+
+??? info "{{m.get('name')}}"
+{% if m.documentation|length>0 %}
+ DOC:{{m.documentation|node_text_contents_to_string|trim}}
+{% endif %}
+{% if m.argument|length>0 %}
+ Arguments:
+{%- for arg in m.argument %}
+
+ - {{arg.get('name')}} ({{arg.get('dataType')}} {% if arg.array|length>0 %}[]{% endif %})
+ {%- if arg.documentation|length>0 -%}
+ :{{arg.documentation|node_text_contents_to_string|trim}}
+ {%- endif -%}
+{% endfor %}
+{% endif -%}
+{%- if m.returnvalue|length>0 %}
+
+ Return values:
+{% for rv in m.returnvalue %}
+ - {{rv.get('name')}} ({{rv.get('dataType')}} {%- if rv.array|length>0 %}[]{% endif %})
+ {%- if rv.documentation|length>0 -%}
+ :{{rv.documentation|node_text_contents_to_string|trim}}
+ {% endif %}
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% endif %}
+ {% else %}
+ This quasar class has no address-space components.
+ {% endif %}
+{% if cls.hasobjects|length>0 %}
+
+Classes possible to be children objects:
+{% for ho in cls.hasobjects %}
+- {{ho.get('class')}}
+{% endfor %}
+{% endif %}
+{% endfor %}
diff --git a/Configuration/templates/designToConfigDocumentationMd.jinja b/Configuration/templates/designToConfigDocumentationMd.jinja
new file mode 100755
index 00000000..eea8ef38
--- /dev/null
+++ b/Configuration/templates/designToConfigDocumentationMd.jinja
@@ -0,0 +1,164 @@
+{# Copyright CERN, 2015. #}
+{# All rights not expressly granted are reserved. #}
+{# This file is part of Quasar. #}
+{# #}
+{# Quasar is free software: you can redistribute it and/or modify #}
+{# it under the terms of the GNU Lesser General Public Licence as published by #}
+{# the Free Software Foundation, either version 3 of the Licence. #}
+{# Quasar is distributed in the hope that it will be useful, #}
+{# but WITHOUT ANY WARRANTY; without even the implied warranty of #}
+{# #}
+{# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #}
+{# GNU Lesser General Public Licence for more details. #}
+{# #}
+{# You should have received a copy of the GNU Lesser General Public License #}
+{# along with Quasar. If not, see #}
+{# #}
+{# Created: Jul 2014 #}
+{# Mar 2020 (translated to Jinja2) #}
+{# Deb 2024 (converted to Md) #}
+{# Authors: #}
+{# Piotr Nikiel #}
+{# Ben Farnham #}
+{# Etienne Fortin #}
+{% import 'headers.jinja' as headers %}
+{% set this = designInspector.objectify_root() %}
+
+{% macro writeRestrictions(containingClassName, objectName, what) %}
+ {%- set restrictions=designInspector.get_restrictions(containingClassName, objectName, what) %}
+
+ {%- if restrictions|length>0 -%}
+ Value restrictions:
+ {% set restrictionType = oracle.classify_xsd_restrictions(restrictions) %}
+ {%- if restrictionType == 'enumeration' -%}
+ Enumeration:
+
+ {% for restriction in restrictions -%}
+ - {{restriction[1]|trim}}
+ {% endfor %}
+ {%- elif restrictionType == 'pattern' -%}
+ Pattern:
+
+ {% for restriction in restrictions -%}
+ - {{restriction[1]|trim}}
+ {% endfor %}
+ {%- elif restrictionType == 'bounds' -%}
+ Bounds:
+
+ {% for restriction in restrictions|sort(attribute=0, reverse=True) -%}
+ {%- if restriction[0] == 'minExclusive' -%}
+ - {{boundsRestrictionPrefix}}{{objectName}}>{{restriction[1]}}
+ {% endif %}
+ {%- if restriction[0] == 'maxExclusive' -%}
+ - {{boundsRestrictionPrefix}}{{objectName}}<{{restriction[1]}}
+ {% endif %}
+ {%- if restriction[0] == 'minInclusive' -%}
+ - {{boundsRestrictionPrefix}}{{objectName}}>={{restriction[1]}}
+ {% endif %}
+ {%- if restriction[0] == 'maxInclusive' -%}
+ - {{boundsRestrictionPrefix}}{{objectName}}<={{restriction[1]}}
+ {% endif %}
+ {%- endfor %}
+ {% endif %}
+ {% endif %}
+{% endmacro %}
+
+{% macro writeCacheVarOrConfigEntry(containingClassName, elementObject, what) %}
+
+{% set objectName = elementObject.get('name') %}
+{% set objectType = elementObject.get('dataType') %}
+{% set optionalityMsg = 'optional, the default value shown below will be used if not present' if elementObject.get('defaultValue') else 'mandatory' %}
+??? info "{{objectName}} ({{objectType}}) {{optionalityMsg}}"
+{% set docElements = designInspector.objectifyDocumentation(containingClassName, objectName) %}
+{% if docElements|length > 0 %}
+ DOC:
+ {%- for docElement in docElements %}
+ {{- designInspector.strip_documentation_tag(docElement)|trim }}
+ {% endfor %}
+ {# Add to the docs the enumeration documentation, if any. #}
+ {% if what == 'configentry' %}
+ {% set configEntryObjectified = designInspector.objectify_config_entries(containingClassName, "[@name='" + objectName + "']")[0] %}
+ {% set enumerationValuesWithDocs = designInspector.xpath_relative_to_object(configEntryObjectified, 'd:configRestriction/d:restrictionByEnumeration/d:enumerationValue[d:documentation]') %}
+ {% if enumerationValuesWithDocs|length > 0 %}
+
+ {% for enumerationValue in designInspector.xpath_relative_to_object(configEntryObjectified, 'd:configRestriction/d:restrictionByEnumeration/d:enumerationValue') %}
+ {{enumerationValue.get('value')}}
:
+ {% if enumerationValue.getchildren()|length > 0 %}
+ {{ designInspector.strip_documentation_tag(enumerationValue.getchildren()[0]) }}
+ {% else %}
+ To the developer: complete this documentation entry in your Design.
+ {% endif %}
+
+ {% endfor %}
+
+ {% endif %}
+ {% endif %}
+ {% endif %}
+
+ {% if elementObject.get('defaultValue') -%}
+ Default value: {{elementObject.get('defaultValue')|trim}}
+ {% endif %}
+ {{- writeRestrictions(containingClassName, objectName, what) }}
+{% endmacro %}
+
+#Configuration documentation for {{designInspector.getProjectName()}}
+
+{#
+ Jump to:
+ {% for className in designInspector.get_names_of_all_classes()|sort() %}
+ {% set parentObjects = designInspector.objectifyAllParents(className, restrict_to_by_configuration=True) %}
+ {% if parentObjects|length > 0 %}
+ {{className}}
+ {% endif %}
+ {% endfor %}
+#}
+
+{% for className in designInspector.get_names_of_all_classes()|sort() %}
+{% set parentObjects = designInspector.objectifyAllParents(className, restrict_to_by_configuration=True) %}
+{% if parentObjects|length > 0 %}
+---
+##{{className}}
+{% set documentation = designInspector.objectifyDocumentation(className) %}
+{% if documentation %}
+{% for docElement in documentation %}
+DOC:{{ designInspector.strip_documentation_tag(docElement)|trim }}
+{% endfor %}
+{% endif %}
+{% if designInspector.is_class_always_singleton(className) %}
+!!! warning This class is a singleton, do not declare it more than once
+{% if designInspector.get_class_default_instance_name(className) != None %}
+!!! info The name
attribute in this class is defaulted to {{designInspector.get_class_default_instance_name(className)}}
, don't waste time specifying it in the config file.
+{% endif %}
+{% endif %}
+
+Configuration attributes:
+{% set configEntries= designInspector.objectify_config_entries(className) %}
+{% if configEntries|length > 0 %}
+ {% for configEntry in configEntries %}
+ {{- writeCacheVarOrConfigEntry(className, configEntry, "configentry") }}
+ {% endfor %}
+{% else %}
+ Class {{className}} has no configuration entries.
+{% endif %}
+
+{% set configuredCacheVars = designInspector.objectify_cache_variables(className, restrict_by="[@initializeWith='configuration']") %}
+{% if configuredCacheVars |length > 0 %}
+ {% for cacheVar in configuredCacheVars %}
+ {{- writeCacheVarOrConfigEntry(className, cacheVar, "cachevariable") }}
+ {% endfor %}
+{%- else %}
+Class {{className}} has no cache variables initialized from the configuration.
+{% endif %}
+
+Possible children:
+
+ - CalculatedVariable
+ - FreeVariable
+ {% for hasObject in designInspector.get_class_has_objects(className) -%}
+ {%- if hasObject.get('instantiateUsing')=='configuration' -%}
+ - {{hasObject.get('class')|trim}}
+ {% endif %}
+ {%- endfor %}
+
+{% endif %}
+{% endfor %}
diff --git a/FrameworkInternals/quasarCommands.py b/FrameworkInternals/quasarCommands.py
index 4298dfdf..d0fce277 100644
--- a/FrameworkInternals/quasarCommands.py
+++ b/FrameworkInternals/quasarCommands.py
@@ -60,7 +60,9 @@
[['generate','config_cpp'], lambda context: transformByKey(TransformKeys.CONFIGURATOR, {'context':context}), False],
[['generate','config_validator'], lambda context: transformByKey(TransformKeys.CONFIG_VALIDATOR, {'context':context}), False],
[['generate','config_doc'], lambda context: transformByKey(TransformKeys.CONFIG_DOCUMENTATION, {'context':context}), True],
- [['generate','as_doc'], lambda context: transformByKey(TransformKeys.AS_DOCUMENTATION, {'context':context}), True],
+ [['generate','as_doc'], lambda context: transformByKey(TransformKeys.AS_DOCUMENTATION, {'context':context}), True],
+ [['generate','config_doc_md'], lambda context: transformByKey(TransformKeys.CONFIG_DOCUMENTATION_MD, {'context':context}), True],
+ [['generate','as_doc_md'], lambda context: transformByKey(TransformKeys.AS_DOCUMENTATION_MD, {'context':context}), True],
[['generate','diagram'], createDiagram, True],
[['check_consistency'], mfCheckConsistency, True],
[['setup_svn_ignore'], mfSetupSvnIgnore, True],
diff --git a/FrameworkInternals/transformDesign.py b/FrameworkInternals/transformDesign.py
index 8a88c678..355c05cc 100644
--- a/FrameworkInternals/transformDesign.py
+++ b/FrameworkInternals/transformDesign.py
@@ -59,6 +59,8 @@ class TransformKeys(enum.Enum):
D_CMAKE = 22
CONFIG_DOCUMENTATION = 23
AS_DOCUMENTATION = 24
+ CONFIG_DOCUMENTATION_MD = 25
+ AS_DOCUMENTATION_MD = 26
# column IDs
@enum.unique
@@ -95,7 +97,9 @@ class FieldIds(enum.Enum):
[TransformKeys.D_BASE_CMAKE, ['Device','designToGeneratedCmakeDeviceBase.jinja'], True, 'Device/generated/cmake_device_base_header.cmake', 'B', False, False, None],
[TransformKeys.D_CMAKE, ['Device','designToGeneratedCmakeDevice.jinja'], False, 'Device/generated/cmake_device_header.cmake', 'B', False, False, None],
[TransformKeys.CONFIG_DOCUMENTATION, ['Configuration','designToConfigDocumentationHtml.jinja'], True, 'Documentation/ConfigDocumentation.html', 'S', False, False, None],
- [TransformKeys.AS_DOCUMENTATION, ['AddressSpace','designToAddressSpaceDocHtml.jinja'], True, 'Documentation/AddressSpaceDoc.html', 'S', False, False, None]
+ [TransformKeys.AS_DOCUMENTATION, ['AddressSpace','designToAddressSpaceDocHtml.jinja'], True, 'Documentation/AddressSpaceDoc.html', 'S', False, False, None],
+ [TransformKeys.CONFIG_DOCUMENTATION_MD, ['Configuration','designToConfigDocumentationMd.jinja'], False, 'Documentation/ConfigDocumentation.md', 'S', False, False, None],
+ [TransformKeys.AS_DOCUMENTATION_MD, ['AddressSpace','designToAddressSpaceDocMd.jinja'], False, 'Documentation/AddressSpaceDoc.md', 'S', False, False, None]
]
def transformDesignVerbose(transformPath, designXmlPath, outputFile, requiresMerge, astyleRun=False, additionalParam=None):