Skip to content

Commit

Permalink
Merge pull request #322 from quasar-team/OPCUA-3194_generate_markdown…
Browse files Browse the repository at this point in the history
…_documentation

Adding markdown documentation generation
  • Loading branch information
parasxos authored Sep 6, 2024
2 parents 1083f91 + 495df68 commit ef0e2e1
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 2 deletions.
127 changes: 127 additions & 0 deletions AddressSpace/templates/designToAddressSpaceDocMd.jinja
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/> #}
{# #}
{# Created: Sep 2018 (the original XSLT version) #}
{# 11 May 2020 (translated to Jinja2) #}
{# 05 February 2024 (converted to Markdown) #}
{# Authors: #}
{# Piotr Nikiel <[email protected]> #}

{% 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) %}
<a href="#class_{{cls.get('name')}}">{{cls.get('name')}}</a>
{% 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<br/>
{% else %}
source-variable<br/>
{%- endif %}
Access=
{{-estimateFunctionReadWrite(variable)-}}<br/>
{% 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}}<br/>
{% endif %}
{% if m.argument|length>0 %}
Arguments:<br/>
{%- 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}}<br/>
{%- endif -%}<br/>
{% endfor %}
{% endif -%}
{%- if m.returnvalue|length>0 %}

Return values:<br/>
{% 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}}<br/>
{% 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:<br>
{% for ho in cls.hasobjects %}
- {{ho.get('class')}}<br>
{% endfor %}
{% endif %}
{% endfor %}
164 changes: 164 additions & 0 deletions Configuration/templates/designToConfigDocumentationMd.jinja
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/> #}
{# #}
{# Created: Jul 2014 #}
{# Mar 2020 (translated to Jinja2) #}
{# Deb 2024 (converted to Md) #}
{# Authors: #}
{# Piotr Nikiel <[email protected]> #}
{# Ben Farnham <[email protected]> #}
{# Etienne Fortin <[email protected]> #}
{% 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:<br>
{% 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}}&gt;{{restriction[1]}}
{% endif %}
{%- if restriction[0] == 'maxExclusive' -%}
- {{boundsRestrictionPrefix}}{{objectName}}&lt;{{restriction[1]}}
{% endif %}
{%- if restriction[0] == 'minInclusive' -%}
- {{boundsRestrictionPrefix}}{{objectName}}&gt;={{restriction[1]}}
{% endif %}
{%- if restriction[0] == 'maxInclusive' -%}
- {{boundsRestrictionPrefix}}{{objectName}}&lt;={{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 %}
<ul>
{% for enumerationValue in designInspector.xpath_relative_to_object(configEntryObjectified, 'd:configRestriction/d:restrictionByEnumeration/d:enumerationValue') %}
<li><code>{{enumerationValue.get('value')}}</code>:
{% if enumerationValue.getchildren()|length > 0 %}
{{ designInspector.strip_documentation_tag(enumerationValue.getchildren()[0]) }}
{% else %}
<b>To the developer: complete this documentation entry in your Design.</b>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
{% endif %}

{% if elementObject.get('defaultValue') -%}
Default value: {{elementObject.get('defaultValue')|trim}}<br>
{% 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 %}
<a href="#class_{{className}}">{{className}}</a>
{% 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 }}<br>
{% 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 <code>name</code> attribute in this class is defaulted to <code>{{designInspector.get_class_default_instance_name(className)}}</code>, 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 %}
4 changes: 3 additions & 1 deletion FrameworkInternals/quasarCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
6 changes: 5 additions & 1 deletion FrameworkInternals/transformDesign.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit ef0e2e1

Please sign in to comment.