Repository: ansys/pyconverter-xml2py (language: python)

Issue:
Implement a pre-defined folder format
### 📝 Description of the feature

Using a pre-defined format in order to generalize the Dita-AST converter.

The structure would look like this:
![organisation](https://user-images.githubusercontent.com/78221213/233687405-eef68112-8c14-4b52-bd13-94eb9c654436.png)


### 💡 Steps for implementing the feature

_No response_

### 🔗 Useful links and references

_No response_


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml
index 230dd8cf3..a7ab2ae04 100644
--- a/.github/workflows/ci_cd.yml
+++ b/.github/workflows/ci_cd.yml
@@ -18,6 +18,7 @@ env:
   PACKAGE_NAME: 'ansys-dita-ast'
   PACKAGE_NAMESPACE: 'ansys.dita.ast'
   RESET_PIP_CACHE: 0
+  ON_CI: true
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
@@ -102,15 +103,16 @@ jobs:
           key: Python-v${{ env.RESET_PIP_CACHE }}-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}
           restore-keys: |
             Python-v${{ env.RESET_PIP_CACHE }}-${{ runner.os }}
-
+    
       # The token will expire in 2024.
-      - name: "Checkout pydita-ast repository"
+      - name: "Checkout mapdl-cmd-doc repository"
         uses: actions/checkout@v3
         with:
-          repository: pyansys/pydita-ast
-          token: ${{ secrets.DITA_AST_DOC_TOKEN }}
-          path: pydita-ast
-        
+          repository: pyansys/mapdl-cmd-doc
+          ref: feat/pydita-ast-predined-format
+          token: ${{ secrets.MAPDL_CMD_DOC_TOKEN }}
+          path: mapdl-cmd-doc
+
       - name: "Unit testing requirements installation"  
         run: |
           python -m pip install .[tests]
@@ -118,8 +120,9 @@ jobs:
       - name: "Unit testing"
         run: |
           pytest -v --durations=10 --maxfail=10 \
-            --reruns 7 --reruns-delay 3 --ghdir ${{ github.workspace }} \
-            --cov=ansys.dita.ast --cov-report=xml:centos-remote.xml --cov-report=html
+            --reruns 7 --reruns-delay 3 --ghdir ${{ github.workspace }}\
+            --cov=ansys.dita.ast --cov-report=xml:coverage.xml --cov-report=html\
+            --cov-report term
 
       - name: "Upload coverage to Codecov"
         uses: codecov/codecov-action@v3
diff --git a/.gitignore b/.gitignore
index 97a53435a..decd19818 100644
--- a/.gitignore
+++ b/.gitignore
@@ -152,7 +152,15 @@ cython_debug/
 #  JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
 #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
 #  and can be added to the global gitignore or merged into this file.  For a more nuclear
-#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#  option (not recommended) you can uncomment the following to ignore the entire idea directory.
 #.idea/
 
 # End of https://www.toptal.com/developers/gitignore/api/python
+
+#VSCode environment
+.vscode
+
+
+helper/
+
+package/
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 02f94fcd6..5523c67de 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,7 +5,7 @@ repos:
   hooks:
   - id: black
     args:
-      - --line-length=88
+      - --line-length=100
 
 - repo: https://github.com/adamchainz/blacken-docs
   rev: v1.12.1
@@ -13,7 +13,7 @@ repos:
   - id: blacken-docs
     additional_dependencies: [black==22.12.0]
     args:
-      - --line-length=88
+      - --line-length=100
 
 - repo: https://github.com/pycqa/isort
   rev: 5.12.0
@@ -25,7 +25,7 @@ repos:
   hooks:
   - id: flake8
     args:
-      - --max-line-length=88
+      - --max-line-length=100
 
 - repo: https://github.com/pre-commit/pre-commit-hooks
   rev: v4.4.0
diff --git a/README.rst b/README.rst
index d7f02d359..08327b3e2 100644
--- a/README.rst
+++ b/README.rst
@@ -92,7 +92,7 @@ For building documentation, you can either run the usual rules provided in the
 
 .. code:: bash
 
-    python -m pip install -r requirements/requirements_doc.txt
+    python -m pip install .[doc]
     make -C doc/ html
 
     # subsequently open the documentation with (under Linux):
diff --git a/_package/AUTHORS.md b/_package/AUTHORS.md
new file mode 100644
index 000000000..c9c30017d
--- /dev/null
+++ b/_package/AUTHORS.md
@@ -0,0 +1,9 @@
+# Authors
+
+## Project Lead
+
+*   [To be completed](https://github.com/)
+
+## Contributors
+
+*   [To be completed](https://github.com/)
diff --git a/_package/LICENSE b/_package/LICENSE
new file mode 100644
index 000000000..9f93a3843
--- /dev/null
+++ b/_package/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 ANSYS, Inc. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/_package/README.rst b/_package/README.rst
new file mode 100644
index 000000000..d625506c8
--- /dev/null
+++ b/_package/README.rst
@@ -0,0 +1,6 @@
+Documentation
+=============
+
+Autogenerated documentation.
+
+
diff --git a/_package/doc/.vale.ini b/_package/doc/.vale.ini
new file mode 100644
index 000000000..e3d2e7bf2
--- /dev/null
+++ b/_package/doc/.vale.ini
@@ -0,0 +1,28 @@
+# Core settings
+# =============
+
+# Location of our `styles`
+StylesPath = "styles"
+
+# The options are `suggestion`, `warning`, or `error` (defaults to “warning”).
+MinAlertLevel = warning
+
+# By default, `code` and `tt` are ignored.
+IgnoredScopes = code, tt
+
+# By default, `script`, `style`, `pre`, and `figure` are ignored.
+SkippedScopes = script, style, pre, figure
+
+# WordTemplate specifies what Vale will consider to be an individual word.
+WordTemplate = \b(?:%s)\b
+
+# List of Packages to be used for our guidelines
+Packages = Google
+
+[*.{md,rst}]
+
+# Apply the following styles
+BasedOnStyles = Vale, Google
+
+# Removing Google-specific rule - Not applicable under some circumstances
+Google.Colons = NO
\ No newline at end of file
diff --git a/_package/doc/Makefile b/_package/doc/Makefile
new file mode 100644
index 000000000..7a41f4386
--- /dev/null
+++ b/_package/doc/Makefile
@@ -0,0 +1,36 @@
+# Minimal makefile for Sphinx documentation
+#
+
+export PYVISTA_OFF_SCREEN := True
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+SPHINXPROJ    = package
+SOURCEDIR     = source
+BUILDDIR      = _build
+
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+clean:
+	rm -rf $(BUILDDIR)/*
+	rm -rf _autosummary
+
+# remove autosummary files
+clean-autosummary:
+	find . -type d -name "_autosummary" -exec rm -rf {} +
+
+# build html docs in parallel using all available CPUs
+# WARNING: this is a resource hog
+phtml:
+	$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -j auto
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/_package/doc/make.bat b/_package/doc/make.bat
new file mode 100644
index 000000000..6e8eafafa
--- /dev/null
+++ b/_package/doc/make.bat
@@ -0,0 +1,61 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=_build
+set LINKCHECKDIR=\%BUILDDIR%\linkcheck
+
+if "%1" == "" goto help
+if "%1" == "clean" goto clean
+if "%1" == "clean-all" goto clean-all
+if "%1" == "clean-examples" goto clean-examples
+if "%1" == "linkcheck" goto linkcheck
+
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.http://sphinx-doc.org/
+	exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:clean
+rmdir /s /q %BUILDDIR% > /NUL 2>&1 
+for /d /r %SOURCEDIR% %%d in (_autosummary) do @if exist "%%d" rmdir /s /q "%%d"
+goto end
+
+:clean-all
+rmdir /s /q %BUILDDIR% > /NUL 2>&1 
+rmdir /s /q source\examples\gallery_examples > /NUL 2>&1 
+for /d /r %SOURCEDIR% %%d in (_autosummary) do @if exist "%%d" rmdir /s /q "%%d"
+goto end
+
+:clean-examples
+rmdir /s /q source\examples\gallery_examples > /NUL 2>&1
+goto end
+
+:linkcheck
+%SPHINXBUILD% -b %1 %SPHINXOPTS% %SOURCEDIR% %LINKCHECKDIR%
+echo "Check finished. Report is in %LINKCHECKDIR%."
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/_package/doc/source/_templates/base.rst b/_package/doc/source/_templates/base.rst
new file mode 100644
index 000000000..5536fa108
--- /dev/null
+++ b/_package/doc/source/_templates/base.rst
@@ -0,0 +1,5 @@
+{{ name | escape | underline}}
+
+.. currentmodule:: {{ module }}
+
+.. auto{{ objtype }}:: {{ objname }}
diff --git a/_package/doc/source/conf.py b/_package/doc/source/conf.py
new file mode 100644
index 000000000..f80e6e571
--- /dev/null
+++ b/_package/doc/source/conf.py
@@ -0,0 +1,87 @@
+from datetime import datetime
+
+# -- General configuration ------------------------------------------------
+
+extensions = ["pyvista.ext.plot_directive"]
+templates_path = ["_templates"]
+source_suffix = ".rst"
+master_doc = "index"
+
+project = "Documentation"
+copyright = f"(c) {datetime.now().year} ANSYS, Inc. All rights reserved"
+author = "ANSYS Inc."
+
+release = version = "0.1.dev"
+exclude_patterns = ["_build"]
+pygments_style = "sphinx"
+
+extensions = [
+    "notfound.extension",
+    "numpydoc",
+    "sphinx.ext.autodoc",
+    "sphinx.ext.autosummary",
+    "sphinx.ext.autosectionlabel",
+    "sphinx.ext.coverage",
+    "sphinx.ext.doctest",
+    "sphinx.ext.extlinks",
+    "sphinx.ext.intersphinx",
+    "sphinx_copybutton",
+    "sphinx.ext.mathjax",
+    "linuxdoc.rstFlatTable",
+]
+
+# # Intersphinx mapping
+# intersphinx_mapping = {
+#     "python": ("https://docs.python.org/dev", None),
+#     "scipy": ("https://docs.scipy.org/doc/scipy/", None),
+#     "numpy": ("https://numpy.org/devdocs", None),
+#     "matplotlib": ("https://matplotlib.org/stable", None),
+#     "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None),
+#     "pyvista": ("https://docs.pyvista.org/", None),
+#     "grpc": ("https://grpc.github.io/grpc/python/", None),
+# }
+
+# The suffix(es) of source filenames.
+source_suffix = ".rst"
+language = "en"
+
+# Copy button customization ---------------------------------------------------
+# exclude traditional Python prompts from the copied code
+copybutton_prompt_text = r">>> ?|\.\.\. "
+copybutton_prompt_is_regexp = True
+
+# numpydoc configuration ------------------------------------------------------
+numpydoc_use_plots = True
+numpydoc_show_class_members = False
+numpydoc_xref_param_type = True
+numpydoc_validate = True
+numpydoc_validation_checks = {
+    "GL06",  # Found unknown section
+    "GL07",  # Sections are in the wrong order.
+    "GL08",  # The object does not have a docstring
+    "GL09",  # Deprecation warning should precede extended summary
+    "GL10",  # reST directives {directives} must be followed by two colons
+    "SS01",  # No summary found
+    "SS02",  # Summary does not start with a capital letter
+    # "SS03", # Summary does not end with a period
+    "SS04",  # Summary contains heading whitespaces
+    # "SS05", # Summary must start with infinitive verb, not third person
+    "RT02",  # The first line of the Returns section should contain only the
+    # type, unless multiple values are being returned"
+}
+
+
+# -- Options for HTML output -------------------------------------------------
+html_short_title = html_title = "Documentation"
+html_theme = "pydata_sphinx_theme"
+html_theme_options = {
+    "show_prev_next": False,
+}
+
+html_context = {
+    "display_github": True,  # Integrate GitHub
+    "github_user": "pyansys",
+    "github_repo": "pydita-ast",
+    "github_version": "main",
+}
+# -- Options for HTMLHelp output ---------------------------------------------
diff --git a/_package/doc/source/index.rst b/_package/doc/source/index.rst
new file mode 100644
index 000000000..dbc19ab4a
--- /dev/null
+++ b/_package/doc/source/index.rst
@@ -0,0 +1,8 @@
+Command Generator
+=================
+
+.. toctree::
+   :maxdepth: 1
+
+   docs
+
diff --git a/_package/doc/styles/.gitignore b/_package/doc/styles/.gitignore
new file mode 100644
index 000000000..080f12aa4
--- /dev/null
+++ b/_package/doc/styles/.gitignore
@@ -0,0 +1,4 @@
+*
+!Vocab
+!Vocab/**
+!.gitignore
\ No newline at end of file
diff --git a/_package/doc/styles/Vocab/ANSYS/accept.txt b/_package/doc/styles/Vocab/ANSYS/accept.txt
new file mode 100644
index 000000000..95367425d
--- /dev/null
+++ b/_package/doc/styles/Vocab/ANSYS/accept.txt
@@ -0,0 +1,3 @@
+ANSYS
+Ansys
+ansys
\ No newline at end of file
diff --git a/_package/doc/styles/Vocab/ANSYS/reject.txt b/_package/doc/styles/Vocab/ANSYS/reject.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/_package/pyproject.toml b/_package/pyproject.toml
new file mode 100644
index 000000000..89cac9f52
--- /dev/null
+++ b/_package/pyproject.toml
@@ -0,0 +1,84 @@
+[build-system]
+requires = ["flit_core >=3.2,<4"]
+build-backend = "flit_core.buildapi"
+
+[project]
+# Check https://flit.readthedocs.io/en/latest/pyproject_toml.html for all available sections
+name = "ansys-dita-generatedcommands"
+version = "0.1.dev0"
+description = "Documentation."
+readme = "README.rst"
+requires-python = ">=3.7"
+license = {file = "LICENSE"}
+authors = [
+    {name = "Ansys, Inc.", email = "pyansys.maintainers@ansys.com"},
+]
+maintainers = [
+    {name = "PyAnsys developers", email = "pyansys.maintainers@ansys.com"},
+]
+
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "Programming Language :: Python :: 3",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: OS Independent",
+]
+dependencies = [
+    "importlib-metadata >=4.0",
+    "lxml==4.9.2",
+    "py-asciimath==0.3.0",
+    "pylatexenc==2.10",
+    "tqdm>=4.64.1",
+    "flit>=3.8.0",
+    "sphinx-notfound-page==0.8.3",
+    "numpydoc==1.5.0",
+    "sphinx-copybutton==0.5.1",
+    "build>= 0.10.0",
+    "linuxdoc",
+]
+
+
+
+[project.optional-dependencies]
+tests = [
+    "pytest==7.2.2",
+    "pytest-cov==4.0.0",
+    "pytest-rerunfailures==11.1.2",
+]
+
+doc = [
+    "Sphinx==6.2.1",
+    "pydata_sphinx_theme==0.13.3",
+    "jupyter_sphinx==0.4.0",
+    "numpydoc==1.5.0",
+    "pandas==1.5.3",
+    "pypandoc==1.11",
+    "pytest-sphinx==0.5.0",
+    "sphinx-autobuild==2021.3.14",
+    "sphinx-autodoc-typehints==1.22",
+    "sphinx-copybutton==0.5.1",
+    "sphinx-notfound-page==0.8.3",
+    "sphinx-gallery==0.12.2",
+]
+
+[tool.flit.module]
+name = "ansys.dita.generatedcommands"
+
+[project.urls]
+Source = "https://github.com/pyansys/pydita-ast/"
+Tracker = "https://github.com/pyansys/pydita-ast/issues"
+Homepage = "https://github.com/pyansyspydita-ast"
+
+
+[tool.black]
+line-length = 100
+
+[tool.isort]
+profile = "black"
+force_sort_within_sections = true
+line_length = 100
+default_section = "THIRDPARTY"
+src_paths = ["src"]
+
+[tool.coverage.report]
+show_missing = true
\ No newline at end of file
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 4eaaa881b..5ea65495a 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -1,13 +1,14 @@
 """Sphinx documentation configuration file."""
 from datetime import datetime
 
+from ansys.dita.ast import __version__
 from ansys_sphinx_theme import pyansys_logo_black as logo
 
 # Project information
 project = "ansys-dita-ast"
 copyright = f"(c) {datetime.now().year} ANSYS, Inc. All rights reserved"
 author = "ANSYS, Inc."
-release = version = "0.1.dev0"
+release = version = __version__
 
 # Select desired logo, theme, and declare the html title
 html_logo = logo
@@ -67,6 +68,11 @@
     # type, unless multiple values are being returned"
 }
 
+# make rst_epilog a variable, so you can add other epilog parts to it
+rst_epilog = ""
+# Read link all targets from file
+with open("links.rst") as f:
+    rst_epilog += f.read()
 
 # static path
 html_static_path = ["_static"]
diff --git a/doc/source/contributing/index.rst b/doc/source/contributing/index.rst
new file mode 100644
index 000000000..8dd896b10
--- /dev/null
+++ b/doc/source/contributing/index.rst
@@ -0,0 +1,81 @@
+.. _ref_contributing:
+
+Contribute
+==========
+
+Overall guidance on contributing to a PyAnsys library appears in the
+`Contributing <dev_guide_contributing_>`_ topic
+in the *PyAnsys Developer's Guide*. Ensure that you are thoroughly familiar
+with this guide before attempting to contribute to PyDita-AST.
+
+The following contribution information is specific to PyDita-AST.
+
+Clone the repository
+--------------------
+
+Run this code to clone and install the latest version of PyDita-AST in development mode:
+
+.. code:: console
+
+    git clone https://github.com/pyansys/pydita-ast
+    cd pydita-ast
+    python -m pip install --upgrade pip
+    pip install -e .
+
+Post issues
+-----------
+
+Use the `PyDita-AST Issues <pydita_ast_issues_>`_ page to submit questions,
+report bugs, and request new features. When possible, use these issue
+templates:
+
+* Bug report template
+* Feature request template
+
+If your issue does not fit into one of these categories, create your own issue.
+
+To reach the PyAnsys support team, email `pyansys.core@ansys.com <pyansys.core@ansys.com>`_.
+
+
+Build documentation
+-------------------
+
+To build the PyDita-AST documentation locally, in the root directory of the repository, run::
+    
+    pip install .[doc]
+    .\doc\make.bat html 
+
+
+Adhere to code style
+--------------------
+
+PyDita-AST follows the PEP8 standard as outlined in the `PyAnsys Development Guide
+<dev_guide_pyansys_>`_ and implements style checking using
+`pre-commit <precommit_>`_.
+
+To ensure your code meets minimum code styling standards, run this code::
+
+.. code:: console
+
+  pip install pre-commit
+  pre-commit run --all-files
+
+You can also install this as a pre-commit hook by running this code
+
+.. code:: console
+
+  pre-commit install
+
+
+This way, it's not possible for you to push code that fails the style checks
+
+.. code:: text
+
+  $ git commit -am "added my cool feature"
+  black....................................................................Passed
+  blacken-docs.............................................................Passed
+  isort....................................................................Passed
+  flake8...................................................................Passed
+  check for merge conflicts................................................Passed
+  debug statements (python)................................................Passed
+  Validate GitHub Workflows................................................Passed
diff --git a/doc/source/getting_started/index.rst b/doc/source/getting_started/index.rst
new file mode 100644
index 000000000..d4b511bdc
--- /dev/null
+++ b/doc/source/getting_started/index.rst
@@ -0,0 +1,54 @@
+Getting Started
+===============
+
+Pre-defined format
+------------------
+
+The pre-defined XML directory format is as follow:
+
+.. code:: text
+
+    XML_directory/
+    │
+    ├── graphics/
+    │   ├── .gifs files
+    │   └── images files
+    |
+    ├── links/
+    │   └── .db files
+    |
+    ├── terms/
+    │   ├── glb/
+    │   │   ├── variable_file       #default value is build_variables.ent
+    │   │   ├── global_terms_file   #default value is terms_global.ent
+    │   │   └── manual_file         #default value is manuals.ent
+    │   └── character_directory/       #default value is ent/
+    │       └── .ent files
+    └── xml/
+        ├── subdirectorys/
+        │   ├── .xml files
+        │   └── mathgraphics_directory/
+        │       └── .svg files
+        ├── .xml files
+        └── .ent files
+
+
+Converting an `XML-directory`
+-----------------------------
+
+Once the `XML_directory` is correctly organized, the converter can be used.
+
+.. code:: bash
+
+    python xml2rst.py -p XML_directory_path
+
+Once the directory is converted, the Sphinx documentation can be generated.
+The generated documentation is by default contained in the `package` directory.
+
+The following code is to render the documentation as an HTML one from Windows:
+
+.. code:: bash
+
+    cd package
+    pip install -e .[doc,tests] # Using a virtual environment is recommended.
+    .\doc\make.bat html 
diff --git a/doc/source/index.rst b/doc/source/index.rst
index d2824a5f6..120f3c995 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -1,6 +1,12 @@
-..
-   Just reuse the root readme to avoid duplicating the documentation.
-   Provide any documentation specific to your online documentation
-   here.
+PyDita-AST
+==========
 
-.. include:: ../../README.rst
+A Python libray to convert DITA XML documentation to RST.
+
+
+.. toctree::
+   :hidden:
+
+   getting_started/index.rst
+   user_guide/index.rst
+   contributing/index.rst
diff --git a/doc/source/links.rst b/doc/source/links.rst
new file mode 100644
index 000000000..a936cacbe
--- /dev/null
+++ b/doc/source/links.rst
@@ -0,0 +1,15 @@
+
+.. #Pyansys
+.. _pyansys: https://docs.pyansys.com/dev/
+.. _pyansys_support: pyansys.core@ansys.com
+.. _pyansys_core: pyansys.core@ansys.com
+
+.. #PyDita-AST related
+.. _pydita_ast_issues: https://github.com/pyansys/pydita-ast/issues
+
+.. #PyAnsys Developer Guide
+.. _dev_guide_pyansys: https://dev.docs.pyansys.com/
+.. _dev_guide_contributing: https://dev.docs.pyansys.com/how-to/contributing.html
+
+.. # Other tools
+.. _precommit: https://pre-commit.com/
\ No newline at end of file
diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst
new file mode 100644
index 000000000..e2ea6832e
--- /dev/null
+++ b/doc/source/user_guide/index.rst
@@ -0,0 +1,6 @@
+
+User guide
+==========
+
+.. include:: ../../../README.rst
+    :start-line: 6
diff --git a/pyproject.toml b/pyproject.toml
index c96607f02..7d84df145 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -69,17 +69,17 @@ Tracker = "https://github.com/pyansys/pydita-ast/issues"
 Homepage = "https://github.com/pyansys/pydita-ast"
 
 [tool.black]
-line-length = 88
+line-length = 100
 
 [tool.isort]
 profile = "black"
 force_sort_within_sections = true
-line_length = 88
+line_length = 100
 default_section = "THIRDPARTY"
 src_paths = ["src"]
 
 [tool.coverage.run]
-source = ["pyansys/pydita-ast"]
+source = ["scr"]
 
 [tool.coverage.report]
 show_missing = true
diff --git a/src/ansys/dita/ast/__init__.py b/src/ansys/dita/ast/__init__.py
index e1c90136f..8abe22f8a 100644
--- a/src/ansys/dita/ast/__init__.py
+++ b/src/ansys/dita/ast/__init__.py
@@ -8,3 +8,4 @@
     import importlib_metadata
 
 __version__ = importlib_metadata.version(__name__.replace(".", "-"))
+"""PyDita-AST version."""
diff --git a/src/ansys/dita/ast/ast_tree.py b/src/ansys/dita/ast/ast_tree.py
new file mode 100644
index 000000000..0ae6d858f
--- /dev/null
+++ b/src/ansys/dita/ast/ast_tree.py
@@ -0,0 +1,2391 @@
+import logging
+import re
+import textwrap
+import warnings
+
+from lxml.etree import tostring
+from lxml.html import fromstring
+
+CONV_EQN = False
+SUPPORT_TABLES = True
+
+if CONV_EQN:
+    from py_asciimath.translator.translator import MathML2Tex
+    from pylatexenc.latex2text import LatexNodes2Text
+else:
+    pass
+
+logging.getLogger("py_asciimath.utils").setLevel("CRITICAL")
+
+
+# common statements used within the docs to avoid duplication
+CONST = {
+    # "&thetas;": "θ",  # consider replacing with :math:`\theta`
+    "Dtl?": "",
+    "Caret?": "",
+    "Caret1?": "",
+    "Caret 40?": "",
+    '``"``': "``",
+}
+
+CLEANUP = {
+    ",, ": ", ",
+    ", , ": ", ",
+    ",. ": ". ",
+    " , ": ", ",
+    ", )": ")",
+    ",)": ")",
+    "% ``": "``%",  # Ansys variable names should be pulled inside literals
+    "`` %": "%``",  # same
+}
+
+# map APDL command to pymapdl function
+CMD_MAP = {}
+
+# APDL commands to skip
+SKIP = {"*IF", "*ELSE", "C***", "*RETURN"}
+
+
+def to_py_name(name):
+    """Convert to a python compatible name."""
+    name = CMD_MAP.get(name, name.lower())
+    return re.sub(r"[^a-z0-9]", "", name)
+
+
+# ############################################################################
+# Dita_AST functions
+# ############################################################################
+
+
+def multireplace(string, replacements, ignore_case=False):
+    """Given a string and a replacement map, it returns the replaced string.
+
+    Parameters
+    ----------
+    string : str
+        String to execute replacements on
+
+    replacements : dict
+        Replacement dictionary {value to find: value to replace}.
+
+    ignore_case : bool
+        Whether the match should be case insensitive
+
+    Returns
+    -------
+    str
+    """
+    if not replacements:
+        # Edge case that'd produce a funny regex and cause a KeyError
+        return string
+
+    # If case insensitive, we need to normalize the old string so that later a replacement
+    # can be found. For instance with {"HEY": "lol"} we should match and find a replacement for
+    # "hey", "HEY", "hEy", etc.
+    if ignore_case:
+
+        def normalize_old(s):
+            return s.lower()
+
+        re_mode = re.IGNORECASE
+
+    else:
+
+        def normalize_old(s):
+            return s
+
+        re_mode = 0
+
+    replacements = {normalize_old(key): val for key, val in replacements.items()}
+
+    # Place longer ones first to keep shorter substrings from matching where the longer ones
+    # should take place
+    # For instance given the replacements {'ab': 'AB', 'abc': 'ABC'} against the string 'hey abc',
+    # it should produce 'hey ABC' and not 'hey ABc'
+    rep_sorted = sorted(replacements, key=len, reverse=True)
+    rep_escaped = map(re.escape, rep_sorted)
+
+    # Create a big OR regex that matches any of the substrings to replace
+    pattern = re.compile("|".join(rep_escaped), re_mode)
+
+    # For each match, look up the new string in the replacements, being the key the normalized
+    # old string
+    return pattern.sub(lambda match: replacements[normalize_old(match.group(0))], string)
+
+
+def split_trail_alpha(text):
+    """Split a string based on the last tailing non alphanumeric character."""
+    for ii, char in enumerate(text):
+        if not char.isalnum():
+            break
+
+    ii += 1
+
+    return text[:ii], text[ii:]
+
+
+def is_numeric(text):
+    """Return True when a string is numeric."""
+    try:
+        float(text)
+        return True
+    except ValueError:
+        return False
+
+
+class Element:
+    """Base element."""
+
+    def __init__(self, element, parse_children=True):
+        self._element = element
+        self._content = []
+
+        if element.text is not None:
+            content = " ".join(element.text.split())
+            if content:
+                self._content.append(content)
+        if parse_children:
+            for child in element.getchildren():
+                self._content.append(parse_element(child))
+        if element.tail is not None:
+            text = " ".join(element.tail.split())
+            if text:
+                self._content.append(text)
+
+    @property
+    def text_content(self):
+        """Return the text content."""
+        return self._element.text_content()
+
+    @property
+    def raw(self):
+        """Return the raw string of the element."""
+        return tostring(self._element).decode()
+
+    @property
+    def tostring(self):
+        """Return the attributes of the element as a dictionnary."""
+        return tostring(self._element)
+
+    def has_children(self):
+        return bool(len(self._element.getchildren()))
+
+    def get(self, entry):
+        """Get an item from an element."""
+        return self._element.get(entry)
+
+    @property
+    def added(self):
+        """Has the revision flag "added"."""
+        return self.get("revisionflag") == "added"
+
+    @property
+    def any_isalnum(self):
+        """Return True if any characters are alpha numeric."""
+        return any([char.isalnum() for char in str(self)])
+
+    @property
+    def tail(self):
+        return " ".join([str(item) for item in self._content[1:]])
+
+    def print_tree(self):
+        """Print the tree."""
+        print(self.types_repr())
+
+    def types_repr(self):
+        """Return the string types."""
+        return "\n".join([str(type(item)) for item in self])
+
+    def __getitem__(self, index):
+        return self._content[index]
+
+    def __len__(self):
+        return len(self._content)
+
+    @property
+    def children(self):
+        """Children of the tree."""
+        return self._content
+
+    @property
+    def title(self):
+        """Return the element title."""
+        return self.find("Title")
+
+    @property
+    def content(self):
+        """Return text content."""
+        return self._content
+
+    def __repr__(self):
+        return "".join([str(item) for item in self._content])
+
+    @property
+    def id(self):
+        return self._element.get("id")
+
+    def to_rst(self, prefix="", links=None, base_url=None, fcache=None):
+        items = []
+        for item in self:
+            if isinstance(item, Element):
+                if item.tag in item_needing_all:
+                    items.append(
+                        item.to_rst(
+                            prefix,
+                            links=links,
+                            base_url=base_url,
+                            fcache=fcache,
+                        )
+                    )
+                elif item.tag in item_needing_links_base_url:
+                    items.append(item.to_rst(prefix, links=links, base_url=base_url))
+                elif item.tag in item_needing_fcache:
+                    items.append(item.to_rst(prefix=prefix, fcache=fcache))
+                else:
+                    items.append(item.to_rst(prefix=prefix))
+            else:
+                items.append(prefix + str(item))
+        return " ".join(items)
+
+    def rec_find(self, _type, terms=None):
+        """Return the first type matching a given type string recursively."""
+        for item in self:
+            if type(item).__name__ == _type:
+                if _type == "Refname" or _type == "Refnamediv":
+                    item.terms = terms
+                return item
+            if isinstance(item, Element):
+                subitem = item.rec_find(_type)
+                if subitem is not None:
+                    return subitem
+        return None
+
+    def find(self, _type, terms=None):
+        """Return the first type matching a given type string."""
+        for item in self:
+            if type(item).__name__ == _type:
+                if _type == "Refname" or _type == "Refnamediv":
+                    if terms == None:
+                        print("ERROR: terms not defined for a Refname class")
+                    item.terms = terms
+                return item
+        return None
+
+    def find_all(self, _type, recursive=False, terms=None):
+        """Return all types matching a given type string."""
+        items = []
+        for item in self:
+            if type(item).__name__ == _type:
+                if _type == "Refname" or _type == "Refnamediv":
+                    item.terms = terms
+                items.append(item)
+            elif recursive and isinstance(item, Element):
+                items.extend(item.find_all(_type))
+
+        return items
+
+    @property
+    def next_elem(self):
+        """Return the next element."""
+        elem = self._element.getnext()
+        return parse_element(elem) if elem is not None else elem
+
+    @property
+    def prev_elem(self):
+        """Return the previous element."""
+        elem = self._element.getprevious()
+        return parse_element(elem) if elem is not None else elem
+
+    @property
+    def tag(self):
+        """Element tag."""
+        return self._element.tag
+
+
+class ItemizedList(Element):
+    """ItemizedList element."""
+
+    def __repr__(self):
+        return "\n".join([f"* {str(item).strip()}" for item in self])
+
+    def to_rst(self, prefix="", links=None, base_url=None, fcache=None):
+        lines = []
+        for item in self:
+            if isinstance(item, Element):
+                if item.tag in item_needing_all:
+                    item_lines = item.to_rst(
+                        prefix, links=links, base_url=base_url, fcache=fcache
+                    ).splitlines()
+                elif item.tag in item_needing_links_base_url:
+                    item_lines = item.to_rst(prefix, links=links, base_url=base_url).splitlines()
+                elif item.tag in item_needing_fcache:
+                    item_lines = item.to_rst(prefix=prefix, fcache=fcache).splitlines()
+                else:
+                    item_lines = item.to_rst(prefix)
+            else:
+                item_lines = str(item).splitlines()
+
+            if item_lines and isinstance(item, Member):
+                line = (
+                    item_lines[0].to_rst()
+                    if isinstance(item_lines, Element)
+                    else str(item_lines[0])
+                )
+                lines.append(textwrap.indent(line, prefix + "* "))
+                for line in item_lines[1:]:
+                    text = line.to_rst(prefix) if isinstance(line, Element) else str(line)
+                    lines.append(textwrap.indent(text, prefix + "  "))
+            else:
+                lines.extend(item_lines)
+
+        # lists must have at least one line proceeding
+        lines = ["", ""] + lines + [""]
+        return "\n".join(lines)
+
+
+class SimpleList(ItemizedList):
+    """ItemizedList element."""
+
+    def __repr__(self):
+        return "\n".join([f"* {str(item).strip()}" for item in self])
+
+
+class Member(Element):
+    """Simple list member element."""
+
+    pass
+
+    # def to_rst(self, prefix=''):
+    #     text = super().to_rst(prefix)
+    #     return text.replace('\n', '')
+
+    # def to_rst(self, prefix=""):
+    #     items = []
+    #     for item in self:
+    #         if isinstance(item, Element):
+    #             items.append(item.to_rst(prefix))
+    #         else:
+    #             items.append(str(item))
+    #     return " ".join(items)
+
+
+class OrderedList(Element):
+    """Ordered list element."""
+
+    def to_rst(self, prefix="", links=None, base_url=None):
+        prefix += "    "
+        ordered_list = []
+        for item in self:
+            if item.tag in item_needing_links_base_url:
+                rst_item = item.to_rst(prefix, links=links, base_url=base_url)
+            else:
+                rst_item = item.to_rst(prefix)
+            ordered_list.append(rst_item)
+        return "\n".join(ordered_list)
+
+
+class ListItem(Element):
+    """List item element."""
+
+    def to_rst(self, prefix="", links=None, base_url=None, fcache=None):
+        items = []
+        for item in self:
+            if isinstance(item, Element):
+                if item.tag in item_needing_all:
+                    items.append(
+                        item.to_rst(
+                            prefix,
+                            links=links,
+                            base_url=base_url,
+                            fcache=fcache,
+                        )
+                    )
+                elif item.tag in item_needing_links_base_url:
+                    items.append(item.to_rst(prefix, links=links, base_url=base_url))
+                elif item.tag in item_needing_fcache:
+                    items.append(item.to_rst(prefix=prefix, fcache=fcache))
+                else:
+                    items.append(item.to_rst(prefix))
+            else:
+                items.append(str(item))
+        return "\n".join(items)
+
+
+class FileName(Element):
+    """Filename element."""
+
+    def to_rst(self, prefix=""):
+        return f"``{self[0]}`` {self.tail}"
+
+
+class OLink(Element):
+    def __init__(self, element):
+        super().__init__(element)
+
+    @property
+    def targetptr(self):
+        return self.get("targetptr")
+
+    @property
+    def targetdoc(self):
+        return self.get("targetdoc")
+
+    def to_rst(self, prefix="", links=None, base_url=None):
+        key = f"{self.targetptr}"
+        if (links or base_url) is None:
+            print("ERROR in the links or the base_url definitions - OLink class.")
+        if key in links:
+            root_name, root_title, href, text = links[key]
+            link = f"{base_url}{root_name}/{href}"
+            content = self.text_content
+            content = content.replace("\n", "")
+            content = content.replace("\r", "")
+            while "  " in content:
+                content = content.replace("  ", " ")
+            if len(content) > 1 and content[0] == "":
+                content = content[1:]
+            content = text if content == "" else content
+            content = content.strip()
+            tail = self.tail
+            tail = tail.replace("\n", "")
+            tail = tail.replace("\r", "")
+            return f"`{content} <{link}>`_ {self.tail}"
+        # else:
+        #     print(self.targetptr)
+
+        return super().to_rst(prefix)
+
+
+class Paragraph(Element):
+    """Paragraph element."""
+
+    def __repr__(self):
+        lines = [""]
+        lines.append(" ".join([str(item) for item in self._content]))
+        lines.append("\n")
+        return "".join(lines)
+
+    def to_rst(self, prefix="", links=None, base_url=None, fcache=None):
+        """Convert to RST style."""
+        items = []
+        for item in self:
+            if isinstance(item, Element):
+                if isinstance(item, Variablelist):
+                    items.append(
+                        "\n\n"
+                        + item.to_rst(
+                            prefix=prefix,
+                            links=links,
+                            base_url=base_url,
+                            fcache=fcache,
+                        )
+                    )
+                else:
+                    if item.tag in item_needing_all:
+                        items.append(
+                            item.to_rst(
+                                prefix=prefix,
+                                links=links,
+                                base_url=base_url,
+                                fcache=fcache,
+                            )
+                        )
+                    elif item.tag in item_needing_links_base_url:
+                        items.append(item.to_rst(prefix=prefix, links=links, base_url=base_url))
+                    elif item.tag in item_needing_fcache:
+                        items.append(item.to_rst(prefix=prefix, fcache=fcache))
+                    else:
+                        items.append(item.to_rst(prefix=prefix))
+            else:
+                items.append(str(item))
+
+        # if len(items) > 1:
+        #     spaced_items = []
+        #     for item in items:
+        #         if not item.startswith('\n'):
+        #             spaced_items.append(' ' + item)
+        #         else:
+        #             spaced_items.append(item)
+        # else:
+        #     spaced_items = items
+
+        return " ".join(items) + "\n"
+
+
+class Phrase(Element):
+    """Phrase element."""
+
+    def __repr__(self):
+        return " ".join([str(item) for item in self._content])
+
+
+class Structname(Element):
+    """Structname element."""
+
+
+class Title(Element):
+    def __repr__(self):
+        return " ".join([str(item) for item in self._content]) + "\n"
+
+
+class Emphasis(Element):
+    """Emphasis element."""
+
+    @property
+    def role(self):
+        return self._element.get("role")
+
+    def to_rst(self, prefix="", links=None, base_url=None):
+
+        if self.role == "bold":
+            # TODO: this isn't the correct way of making text bold
+            content = f"{self[0]} "
+        elif self.role == "italic":
+            # TODO: this isn't the correct way of making text itallic
+            content = f"`{self[0]}` "
+        # elif self.role == 'var':
+        # content = f"``{self[0]}`` "
+        else:
+            content = f"{self[0]} "
+
+        items = []
+        for item in self[1:]:
+            if isinstance(item, Element):
+                if item.tag in item_needing_links_base_url:
+                    items.append(item.to_rst(prefix, links=links, base_url=base_url))
+                else:
+                    items.append(item.to_rst(prefix))
+            else:
+                items.append(str(item))
+
+        return content + " ".join(items)
+
+
+class Example(Element):
+    """Example element."""
+
+    # def source(self):
+    #     """The program listing of the documentation."""
+    #     for item in self._content:
+    #         if isinstance(item, ProgramListing):
+    #             return item
+    #     return ""
+    pass
+
+
+class InformalExample(Element):
+    """Example element."""
+
+    def __repr__(self):
+        lines = ["\n"]
+        if self.title:
+            lines.append(f"Example:\n")
+        else:
+            lines.append(f"Example: {self.title}\n")
+        lines.extend([f"{line}" for line in self._content])  # self._content.splitlines()
+        return "\n".join(lines)
+
+
+class GuiMenu(Element):
+    """GUI Menu element."""
+
+    pass
+
+
+class Replaceable(Element):
+    """Replaceable element."""
+
+    @property
+    def is_equals(self):
+        """Replaceable equals something."""
+        # return self.tail.startswith("=")
+        return False
+
+    @property
+    def content_equals(self):
+        words = self.tail.split("=")[1].split()
+        if not words:
+            # probably a literal
+            next_elem = self.next_elem
+            parm = next_elem[0]
+            next_elem.content[0] = ""
+        else:
+            parm = words[0]
+
+        if len(words) > 1:
+            tail = " ".join(words[1:])
+        else:
+            tail = ""
+        if parm.startswith('"'):
+            # quoted parm
+            ii = parm[1:].find('"') + 2
+            parm, extra = parm[:ii], parm[ii:]
+        else:
+            parm, extra = split_trail_alpha(parm)
+        tail = f"{extra} {tail}"
+
+        if not is_numeric(parm) and not parm.startswith('"'):
+            parm = f'"{parm}"'
+
+        return f"``{self.content[0].lower()}={parm}`` {tail}"
+
+    def to_rst(self, prefix=""):
+        """Convert to an rst format."""
+        if isinstance(self.prev_elem, Command):
+            if any([self.content[0] in arg for arg in self.prev_elem.args]):
+                return f"{self.tail}"
+        if self.is_equals:
+            return self.content_equals
+        return f"``{self.content[0]}`` {self.tail}"
+
+
+class ProgramListing(Element):
+    """Program listing element."""
+
+    @property
+    def source(self):
+        if self._element.text is None:
+            return "\n".join(str(item) for item in self.content)
+        return self._element.text
+
+    def to_rst(self, prefix=""):
+        header = f"\n\n{prefix}.. code::\n\n"
+        return header + textwrap.indent(self.source, prefix + " " * 3) + "\n"
+
+
+class Variablelist(Element):
+    """Variable list."""
+
+    def to_rst(self, prefix="", links=None, base_url=None, fcache=None):
+        active_items = []
+        for item in self:
+            if isinstance(item, VarlistEntry) and not item.active:
+                continue
+            if isinstance(item, Element):
+                if item.tag in item_needing_all:
+                    active_items.append(
+                        item.to_rst(
+                            prefix,
+                            links=links,
+                            base_url=base_url,
+                            fcache=fcache,
+                        )
+                    )
+                elif item.tag in item_needing_links_base_url:
+                    active_items.append(item.to_rst(prefix, links=links, base_url=base_url))
+                elif item.tag in item_needing_fcache:
+                    active_items.append(item.to_rst(prefix=prefix, fcache=fcache))
+                else:
+                    active_items.append(item.to_rst(prefix))
+            else:
+                active_items.append(str(item))
+        return "\n".join(active_items) + "\n"
+
+    @property
+    def terms(self):
+        return [str(item.term) for item in self if isinstance(item, VarlistEntry)]
+
+
+class RefSection(Element):
+    """Reference Section element."""
+
+    def to_rst(self, prefix="", links=None, base_url=None, fcache=None):
+        """Convert to restructured text."""
+        items = []
+        for item in self[1:]:
+            if isinstance(item, Element):
+                if item.tag in item_needing_all:
+                    items.append(
+                        item.to_rst(
+                            prefix=prefix,
+                            links=links,
+                            base_url=base_url,
+                            fcache=fcache,
+                        )
+                    )
+                elif item.tag in item_needing_links_base_url:
+                    items.append(item.to_rst(prefix=prefix, links=links, base_url=base_url))
+                elif item.tag in item_needing_fcache:
+                    items.append(item.to_rst(prefix=prefix, fcache=fcache))
+                else:
+                    items.append(item.to_rst(prefix=prefix))
+            else:
+                items.append(str(item))
+        return "\n".join(items)
+
+
+class VarlistEntry(Element):
+    """Variable list entry."""
+
+    @property
+    def parm_types(self):
+        """Return the parameter type(s).
+
+        This is guessed from any subvarlists. If unavailable, then it's guessed
+        from the description of the variable.
+
+        This is either a string, float, or int (or some combination thereof).
+
+        """
+        varlist = self.rec_find("Variablelist")
+
+        parm_types = [str]
+        if varlist is not None:
+            terms = varlist.terms
+            if terms:
+                terms_numeric = [is_numeric(term) for term in terms]
+                if all(terms_numeric):
+                    parm_types = [int]
+                elif any(terms_numeric):
+                    parm_types = [int, str]
+                else:
+                    parm_types = [str]
+
+                # consider checking for bool
+                # terms_numeric = set(terms) == set(['1', '0'])
+
+        return parm_types
+
+    @property
+    def term(self):
+        return self.content[0]
+
+    @property
+    def text(self):
+        return self.content[1]
+
+    def py_term(self, links=None, base_url=None):
+        """Python compatible term."""
+        if self.is_arg:
+            arg = str(self.term).lower()
+
+            if arg == "type":
+                arg = "type_"
+
+            if self.parm_types is not None:
+                ptype_str = " or ".join([parm_type.__name__ for parm_type in self.parm_types])
+
+                return f"{arg} : {ptype_str}"
+            return f"{arg}"
+
+        # if self.term.tag in item_needing_all:
+        #     arg = self.term.to_rst(links=links, base_url=base_url, fcache=fcache).replace("--", "").strip() # noqa : E501
+        if self.term.tag in item_needing_links_base_url:
+            arg = self.term.to_rst(links=links, base_url=base_url).replace("--", "").strip()
+        # elif self.term.tag in item_needing_fcache:
+        #     arg = self.term.to_rst(fcache=fcache).replace("--", "").strip()
+        else:
+            arg = self.term.to_rst().replace("--", "").strip()
+
+        # sanity check
+        if "blank" in arg.lower():
+            arg = ""
+
+        if not is_numeric(arg):
+            return f'"{arg}"'
+        return arg
+
+    def __repr__(self):
+        return f"{self.term}\n{self.content[1]}"
+
+    @property
+    def active(self):
+        """Return if this arg is active."""
+        if not self.is_arg:
+            return True
+
+        # otherwise, simply check for any alphanumeric characters in the
+        # term. Normally these terms are just '--' or '--,--'
+        return self.term.any_isalnum
+
+    @property
+    def is_arg(self):
+        """Return True when this variable list is for an argument."""
+        anc = list(self._element.iterancestors())
+        if not anc:
+            return False
+        val = anc[0].tag == "variablelist" and anc[1].tag == "refsynopsisdiv"
+        if not val:
+            # miscoded?
+            elem_id = anc[1].get("id")
+            if elem_id:
+                val = "argdescript" in elem_id
+        return val
+
+    def py_text(self, links=None, base_url=None, fcache=None):
+        """Remove mention of graphical interaction."""
+        if self.text.tag in item_needing_all:
+            rst = self.text.to_rst(links=links, base_url=base_url, fcache=fcache)
+        elif self.text.tag in item_needing_links_base_url:
+            rst = self.text.to_rst(links=links, base_url=base_url)
+        elif self.text.tag in item_needing_fcache:
+            rst = self.text.to_rst(links=links, fcache=fcache)
+        else:
+            rst = self.text.to_rst()
+        if "graphical" in rst:
+            valid = []
+            for sentence in rst.split(". "):
+                if "GUI" not in sentence:
+                    valid.append(sentence)
+            rst = ". ".join(valid)
+        return rst
+
+    def to_rst(self, prefix="", links=None, base_url=None, fcache=None):
+        prefix += "    "
+        # if this is a parameter arg
+        if self.is_arg:
+            lines = [f"{self.py_term(links=links, base_url=base_url)}"]
+            lines.append(
+                textwrap.indent(
+                    self.py_text(links=links, base_url=base_url, fcache=fcache),
+                    prefix,
+                )
+            )
+            return "\n".join(lines)
+
+        # term_text = [line.strip() for line in self.py_text.splitlines()]
+        # term_text = term_text[0] + '\n' + textwrap.indent('\n'.join(term_text[1:]), )
+
+        lines = [
+            f"* ``{self.py_term(links=links, base_url=base_url)}`` - {textwrap.indent(self.py_text(links=links, base_url=base_url, fcache=fcache), prefix)}"  # noqa : E501
+        ]
+        text = "\n".join(lines)
+        # if 'ID number to which this tip belongs' in text:
+        # breakpoint()
+        return text
+
+
+class Term(Element):
+    """Term element."""
+
+    def to_rst(self, prefix="", links=None, base_url=None, fcache=None):
+
+        items = []
+        for item in self:
+            if isinstance(item, Element):
+                if item.tag in item_needing_links_base_url:
+                    items.append(item.to_rst(links=links, base_url=base_url))
+                elif item.tag in item_needing_fcache:
+                    items.append(item.to_rst(fcache=fcache))
+                else:
+                    items.append(item.to_rst())
+            else:
+                items.append(str(item))
+
+        text = ", ".join(items)
+        if text.endswith("–"):
+            text = text[:-1]
+        return text
+
+    def __repr__(self):
+        out = super().__repr__()
+        return ", ".join(out.split(","))
+
+
+class GuiLabel(Element):
+    """GuiLabel element."""
+
+    pass
+
+
+class GuiMenuItem(Element):
+    """GuiMenuItem element."""
+
+    pass
+
+
+class SuperScript(Element):
+    """Superscript element."""
+
+    def to_rst(self, prefix=""):
+        return f":sup:`{self.content[0]}` {self.tail}"
+
+
+class Code(Element):
+    """Code element."""
+
+    pass
+
+
+class _Math(Element):
+    def __init__(self, element):
+        self._element = element
+        self._content = []
+        self._parse_equation(element)
+
+    def _parse_equation(self, eqn):
+        raw = tostring(eqn).decode().replace("xmlns:m", "xmlns")
+        raw = raw.replace('display="block"', "")
+        raw = raw.replace("<mtext>&#8202;</mtext>\n", "")
+        raw = raw.replace("<mtext>&#8201;</mtext>\n", "")
+        try:
+            parsed = MathML2Tex().translate(raw, network=False, from_file=False)
+            parsed = parsed.replace("\\hspace", "").strip()
+            self._content = [parsed]
+        except NameError:
+            self._content = [""]
+
+    def __repr__(self):
+        try:
+            return LatexNodes2Text().latex_to_text(self.equation)
+        except NameError:
+            return "<Equation>"
+
+    @property
+    def equation(self):
+        return self.content[0][1:-1]
+
+
+class Math(_Math):
+    """Math element."""
+
+    def to_rst(self, prefix=""):
+        lines = ["", "", f"{prefix}.. math::\n"]
+        lines.append(textwrap.indent(self.equation, prefix=prefix + " " * 4))
+        lines.append("")
+        return "\n".join(lines)
+
+
+class InlineEquation(_Math):
+    """Inline equation element."""
+
+    def __init__(self, element):
+        self._element = element
+        self._content = []
+        self._parse_equation(element.find("math"))
+
+    @property
+    def tail(self):
+        return self.raw.split("</inlineequation>")[-1].replace("\n", "")
+
+    def to_rst(self, prefix=""):
+        return f":math:`{self.equation.strip()}` {self.tail}"
+
+
+class SubScript(Element):
+    """Inline equation element."""
+
+    def __init__(self, element):
+        super().__init__(element)
+
+
+class InlineGraphic(Element):
+    """Inline graphic element."""
+
+    def fileref(self):
+        """File reference."""
+        return self._element.get("fileref")
+
+
+class Quote(Element):
+    """Quote element."""
+
+    def __init__(self, element):
+        super().__init__(element)
+        # self._content[0] = f"{self._content[0]}"
+
+    @property
+    def quote(self):
+        return self[0]
+
+    def __repr__(self):
+        return f'"{self.quote}" {self.tail}'
+
+
+class Link(Element):
+    """Link element."""
+
+    def __init__(self, element):
+        super().__init__(element)
+        self._linkend = element.get("linkend")
+
+    @property
+    def linkend(self):
+        """Return Link."""
+        return self._linkend
+
+    def __repr__(self):
+        return str(self.linkend)
+
+    def to_rst(self, prefix="", links=None, base_url=None):
+        if (links or base_url) is None:
+            print("ERROR in the links or the base_url definitions - Link class.")
+        tail = " ".join([str(item) for item in self])
+        tail = self.tail.replace("\n", "")
+        if self.linkend in links:
+            root_name, root_title, href, text = links[self._linkend]
+            root_title = root_title.replace("\n", "")
+            link = f"{base_url}{root_name}/{href}"
+            return f"`{root_title} <{link}>`_ {tail}"
+
+        # missing link...
+        return tail
+
+
+class XRef(Link):
+    """XRef element."""
+
+    @property
+    def tail(self):
+        return " ".join([str(item) for item in self._content])
+
+    def to_rst(self, prefix=""):
+        # disabled at the moment
+        # return f':ref:`{self.linkend}`{self.tail}'
+        return self.tail
+
+
+class UserInput(Element):
+    """UserInput."""
+
+    pass
+
+
+class Screen(Element):
+    """Screen output (effectively literal output)."""
+
+    pass
+
+
+class Literal(Element):
+    """Screen output (effectively literal output)."""
+
+    def to_rst(self, prefix=""):
+        return f"``{self.content[0]}`` {self.tail}"
+
+
+class Caution(Element):
+    """Screen output (effectively literal output)."""
+
+    def to_rst(self, prefix=""):
+        lines = ["", "", ".. warning::"]
+        lines.append(textwrap.indent(str(self), prefix=prefix + "   "))
+        return "\n".join(lines)
+
+
+class Graphic(Element):
+    """Graphic element."""
+
+    @property
+    def entityref(self):
+        entityref = self.get("entityref")
+        if entityref is not None:
+            entityref = entityref.strip()
+        return entityref
+
+    def to_rst(self, fcache, prefix=""):
+
+        if self.entityref is None:
+            # probably a math graphics
+            fileref = self.get("fileref")
+            if fileref is not None and "mathgraphics" in fileref:
+                return ""
+
+        if self.entityref == "Linebrk":
+            return f"\n\n"
+
+        if self.entityref in fcache:
+            filename = fcache[self.entityref]
+            text = f"\n\n{prefix}.. figure:: ../images/{filename}\n"
+            return text
+
+        return ""
+
+
+class Function(Element):
+    """Function element."""
+
+    pass
+
+
+class Note(Element):
+    """Note element."""
+
+    pass
+
+
+class BlockQuote(Element):
+    """Note element."""
+
+    def to_rst(self, prefix="", links=None, base_url=None, fcache=None):
+        items = []
+        for item in self:
+            if isinstance(item, Element):
+                if item.tag in item_needing_all:
+                    items.append(
+                        item.to_rst(
+                            prefix,
+                            links=links,
+                            base_url=base_url,
+                            fcache=fcache,
+                        )
+                    )
+                elif item.tag in item_needing_links_base_url:
+                    items.append(item.to_rst(prefix, links=links, base_url=base_url))
+                elif item.tag in item_needing_fcache:
+                    items.append(item.to_rst(prefix=prefix, fcache=fcache))
+                else:
+                    items.append(item.to_rst(prefix))
+            else:
+                items.append(prefix + str(item))
+        return "\n\n" + " ".join(items) + "\n\n"
+
+
+class RefMeta(Element):
+    """Reference metadata element."""
+
+    @property
+    def refentry_title(self):
+        title = self.rec_find("Refentrytitle")
+        if title is not None:
+            return str(title).strip()
+        return ""
+
+
+class IndexTerm(Element):
+
+    pass
+
+
+class Primary(Element):
+
+    pass
+
+
+unsup_elem = set()
+
+
+def parse_element(element):
+    """Parse a single element."""
+    if element.tag not in parsers:
+        if "cython_function" in str(type(element.tag)):
+            return Element(element)
+
+        # warnings.warn(f"Unsupported element tag '{element.tag}'")
+        return Element(element)
+
+    return parsers[element.tag](element)
+
+
+def parse_children(element):
+    """Parse an element."""
+
+    children = []
+    for child in element.getchildren():
+        if child.tag in parsers:
+            children.append(parsers[child.tag](child))
+        else:
+            children.append(child)
+    return children
+
+
+def parse_text(element):
+    """Parse a paragraph element."""
+    if element is not None:
+        return " ".join(element.text_content().split())
+    return ""
+
+
+class TGroup(Element):
+    @property
+    def n_col(self):
+        return self._element.get("cols")
+
+    @property
+    def thead(self):
+        """THead."""
+        return self.find("THead")
+
+    @property
+    def tbody(self):
+        """TBody."""
+        return self.find("TBody")
+
+    @property
+    def row_in_tbody(self):
+        """List of the rows in TBody."""
+        return self.find("TBody").find_all("Row")
+
+    def to_rst(self, prefix="", links=None, base_url=None):
+
+        l_head = 0
+
+        if self.thead is not None:
+            rows, l_head = self.thead.to_rst(links=links, base_url=base_url)
+        else:
+            rows = [".. flat-table::"]
+            rows.append("")
+
+        if self.row_in_tbody is not None:
+
+            rst_tbody = self.tbody.to_rst(l_head, links=links, base_url=base_url)
+            if len(rst_tbody) > 0:
+                rows += rst_tbody
+
+        return "\n".join(rows)
+
+
+class Table(Element):
+    @property
+    def title(self):
+        """Table title."""
+        return self.find("Title")
+
+    @property
+    def tgroup(self):
+        """TGroup."""
+        return self.find("TGroup")
+
+    def to_rst(self, prefix="", links=None, base_url=None):
+        if SUPPORT_TABLES:
+            lines = []
+            if self.title is not None:
+                lines.append(f"{self.title}".strip())
+                lines.append((len(lines[-1]) * "="))
+                lines.append("")
+
+            if self.tgroup is not None:
+                a = self.tgroup
+                lines.append(a.to_rst(prefix=prefix, links=links, base_url=base_url))
+
+            return "\n".join(lines)
+
+        else:
+            return "\nTable redacted."
+
+    def __repr__(self):
+        # This method is limited as the links and the base_url are skiped.
+        return self.to_rst(links={}, base_url=f"pass")
+
+
+class Refentrytitle(Element):
+    """Title."""
+
+    def __repr__(self):
+        items = []
+        for item in self._content:
+            item = str(item)
+            for key, value in CONST.items():
+                item = item.replace(key, value)
+            items.append(item)
+        return "".join(items)
+
+
+class Refnamediv(Element):
+    def __init__(self, element, terms=None):
+        self._element = element
+        self._terms = terms
+        super().__init__(element)
+
+    @property
+    def terms(self):
+        return self._terms
+
+    @terms.setter
+    def terms(self, terms):
+        self._terms = terms
+
+    @property
+    def refname(self):
+        return self.find("Refname", self._terms)
+
+    @property
+    def purpose(self):
+        return self.find("Refpurpose")
+
+
+class Refname(Element):
+    def __init__(self, element, terms=None):
+        self._element = element
+        self._terms = terms
+        super().__init__(element)
+
+    @property
+    def terms(self):
+        return self._terms
+
+    @terms.setter
+    def terms(self, terms):
+        self._terms = terms
+
+    @property
+    def raw_args(self):
+        mapdl_cmd = str(self)
+        mapdl_cmd = mapdl_cmd.replace("&fname_arg;", self._terms["fname_arg"])
+        mapdl_cmd = mapdl_cmd.replace("&fname1_arg;", self._terms["fname1_arg"])
+        mapdl_cmd = mapdl_cmd.replace("&fname2_arg;", self._terms["fname2_arg"])
+        mapdl_cmd = mapdl_cmd.replace("&pn006p;", self._terms["pn006p"])
+        mapdl_cmd = mapdl_cmd.replace("&ansysBrand;", self._terms["ansysBrand"])
+        split_args = mapdl_cmd.split(",")[1:]
+        return split_args
+
+    @property
+    def args(self):
+        args = []
+        for item in self.raw_args:
+            orig_arg = str(item).replace(",", "")
+            arg = orig_arg.lower().replace("--", "").replace("–", "").replace("-", "_").strip()
+            if arg == "":
+                continue
+
+            if arg == "class":
+                arg = "class_"
+            elif arg == "type":
+                arg = "type_"
+
+            # simply check if we can use this as a valid python kwarg
+            try:
+                exec(f"{arg} = 1.0")
+            except SyntaxError:
+                continue
+
+            if "blank" in arg:
+                continue
+
+            args.append(arg)
+
+        # rename duplicate arguments
+        if len(args) != len(set(args)):
+            for arg in args:
+                i = 0
+                if args.count(arg) > 1:
+                    for j in range(len(args)):
+                        if args[j] == arg:
+                            args[j] = f"{arg}{i:d}"
+                            i += 1
+
+        return args
+
+
+class Refpurpose(Element):
+    def __repr__(self):
+        return " ".join([str(item) for item in self._content])
+
+
+class Refclass(Element):
+
+    pass
+
+
+class Application(Element):
+
+    pass
+
+
+class Refsect1(Element):
+
+    pass
+
+
+class Command(Element):
+    """Command element."""
+
+    @property
+    def command(self):
+        return self[0]
+
+    @property
+    def tail_no_args(self):
+        tail = self.tail
+        for arg in self.args:
+            tail = tail.replace(arg, "")
+        return tail
+
+    def __repr__(self):
+        return f"{self.command} {self.tail}"
+
+    @property
+    def has_args(self):
+        # return self.tail.startswith(",")
+        return False
+
+    @property
+    def args(self):
+        """Any terms immediately following the command."""
+        if self.has_args:
+            words = self.tail.split()
+            cmd_args = words[0]
+
+            # Remove trailing non-alphanumeric. These can't be part of the command.
+            while cmd_args and not (cmd_args[-1].isalnum() or cmd_args[-1].isdigit()):
+                cmd_args = cmd_args[:-1]
+
+            # possible that next element is a replaceable and not part of the tail
+            if isinstance(self.next_elem, Replaceable):
+                cmd_args += str(self.next_elem[0])  # no tail
+            elif len(self.tail) > 1 and self.tail[1] == " ":
+                # possible not coded as replacable
+                for word in words[1:]:
+                    if word.upper() == word or is_numeric(word):
+                        if not (word[-1].isalnum() or word[-1].isdigit()):
+                            parm, _ = split_trail_alpha(word)
+                        else:
+                            parm = word
+
+                        if parm:
+                            cmd_args += f",{parm}"
+                    else:
+                        # break at first failing match
+                        break
+
+            return cmd_args.split(",")[1:]
+        return [""]
+
+    @property
+    def py_args(self):
+        args = []
+        for arg in self.args:
+            if is_numeric(arg):
+                args.append(f"{arg}")
+            else:
+                args.append(f'"{arg}"')
+        py_args = ", ".join(args)
+        if py_args == '""':
+            return ""
+        return py_args
+
+    @property
+    def py_cmd(self):
+        return to_py_name(self.command)
+
+    @property
+    def sphinx_cmd(self):
+        if self.has_args:
+            py_args = self.py_args
+        else:
+            py_args = ""
+
+        return f":ref:`{self.py_cmd}`"
+
+    def to_rst(self, prefix=""):
+        """Convert to an rst format."""
+        if self.args and self.args[0] != "":
+            return f"{self.sphinx_cmd} {self.tail_no_args}"
+        else:
+            return f"{self.sphinx_cmd} {self.tail}"
+
+
+class ComputerOutput(Element):
+    def to_rst(self, prefix=""):
+        return f"``{self[0]}`` {self[1]}"
+
+
+class Figure(Element):
+    """Figure element."""
+
+    @property
+    def title(self):
+        return self.rec_find("Title")
+
+    @property
+    def graphic(self):
+        return self.rec_find("Graphic")
+
+    def to_rst(self, prefix="", fcache=None):
+        graphic = self.graphic
+        if graphic is not None and graphic.entityref is not None:
+            lines = []
+            lines.append(graphic.to_rst(prefix=prefix, fcache=fcache))
+            if self.title is not None:
+                lines.append(f"   {self.title}")
+
+            return "\n" + "\n".join(lines) + "\n"
+
+        items = []
+        for item in self:
+            if isinstance(item, Element):
+                if item.tag in item_needing_fcache:
+                    items.append(item.to_rst(prefix=prefix, fcache=fcache))
+                else:
+                    items.append(item.to_rst(prefix))
+            else:
+                items.append(prefix + str(item))
+        return "\n" + "".join(items)
+
+
+class Footnote(Element):
+
+    pass
+
+
+class Footnoteref(Element):
+
+    pass
+
+
+class Formalpara(Element):
+
+    pass
+
+
+class Glossterm(Element):
+
+    pass
+
+
+class Guibutton(Element):
+
+    pass
+
+
+class Guiicon(Element):
+
+    pass
+
+
+class Highlights(Element):
+
+    pass
+
+
+class Important(Element):
+
+    pass
+
+
+class Informalequation(Element):
+
+    pass
+
+
+class Interface(Element):
+
+    pass
+
+
+class Markup(Element):
+
+    pass
+
+
+class Mediaobject(Element):
+
+    pass
+
+
+class Option(Element):
+
+    pass
+
+
+class Refsect3(Element):
+
+    pass
+
+
+class Refsynopsisdiv(Element):
+
+    pass
+
+
+class Sidebar(Element):
+
+    pass
+
+
+class XMLType(Element):
+
+    pass
+
+
+class XMLWarning(Element):
+
+    pass
+
+
+class ClassName(Element):
+
+    pass
+
+
+class Imageobject(Element):
+
+    pass
+
+
+class Informalfigure(Element):
+
+    pass
+
+
+class Envar(Element):
+
+    pass
+
+
+class ImageData(Element):
+
+    pass
+
+
+class ColSpec(Element):
+
+    pass
+
+
+class TBody(Element):
+    @property
+    def rows(self):
+        return self.find_all("Row")
+
+    def to_rst(self, l_head, prefix="", links=None, base_url=None):
+        rst_rows = []
+        for i, row_i in enumerate(self.rows):
+            row = row_i.find_all("Entry")
+            if len(row[0]) > 0:
+                if type(row[0][0]) == Command:
+                    command = f"   * - :ref:`{row[0][0].py_cmd}`"
+                    rst_rows.append(command)
+                    strg = f"     - {row[1][0]}"
+                    rst_rows.append(strg)
+                    if l_head == 0 and i == 0:
+                        l_head = 2
+                else:
+                    if l_head == 0 and i == 0:
+                        row_rst_list = row_i.to_rst_list(links=links, base_url=base_url)
+                        l_head = len(row_rst_list)
+
+                    else:
+                        row_rst_list = row_i.to_rst_list(links=links, base_url=base_url)
+                    row_rst = "\n     - ".join(row_rst_list)
+                    rst_rows.append(f"   * - {row_rst}")
+            elif len(row[1]) > 0:
+                if type(row[1][0]) == Command:
+                    command = f"   * - :ref:`{row[1][0].py_cmd}`"
+                    rst_rows.append(command)
+                    strg = "     - " + str(row[2][0])
+                    rst_rows.append(strg)
+
+        return rst_rows
+
+
+class Entry(Element):
+    @property
+    def morerows(self):
+        return self._element.get("morerows")
+
+    def to_rst(self, prefix="", links=None, base_url=None, fcache=None):
+
+        if self.morerows is not None:
+            content = self.morerows
+
+        items = []
+        for item in self:
+            if isinstance(item, Element):
+                if item.tag in item_needing_links_base_url:
+                    items.append(item.to_rst(prefix, links=links, base_url=base_url))
+                else:
+                    items.append(item.to_rst(prefix))
+            else:
+                items.append(str(item))
+
+        if self.morerows is not None:
+            entry = f":rspan:`{content}` " + " ".join(items)
+
+        else:
+            entry = " ".join(items)
+
+        return entry
+
+
+class Row(Element):
+    @property
+    def entry(self):
+        return self.find_all("Entry")
+
+    def to_rst_list(self, prefix="", links=None, base_url=None):
+        row = []
+        for entry in self.entry:
+            content = entry.to_rst(links=links, base_url=base_url)
+            content = content.replace("\n", "")
+            content = content.replace("\r", "")
+            row.append(content)
+        return row
+
+
+class THead(Element):
+    @property
+    def rows(self):
+        return self.find_all("Row")
+
+    def to_rst(self, prefix="", links=None, base_url=None):
+
+        l_rst_list = 0
+
+        if self.rows is not None:
+            row = []
+
+            if len(self.rows) > 1:
+                row.append(
+                    f".. flat-table:: {''.join(self.rows[0].to_rst_list(links=links, base_url=base_url))}"  # noqa : E501
+                )
+                row.append(f"   :header-rows: {len(self.rows)-1}")
+                ini = 1
+
+            elif len(self.rows[0]) == 1:
+                row.append(self.rows[0].to_rst_list(links=links, base_url=base_url)[0])
+                row.append("")
+                row.append(f".. flat-table::")
+                ini = len(self.rows)
+
+            else:
+                row.append(f".. flat-table::")
+                row.append(f"   :header-rows: 1")
+                ini = 0
+
+            row.append("")
+
+            for i in range(ini, len(self.rows)):
+                rst_list = self.rows[i].to_rst_list(links=links, base_url=base_url)
+                l_rst_list = len(rst_list)
+                row_rst = "\n     - ".join(rst_list)
+                row.append(f"   * - {row_rst}")
+
+            return row, l_rst_list
+
+
+class Remark(Element):
+
+    pass
+
+
+class LiteralLayout(Element):
+
+    pass
+
+
+class CiteTitle(Element):
+
+    pass
+
+
+class ULink(Element):
+
+    pass
+
+
+class SegTitle(Element):
+
+    pass
+
+
+class Chapter(Element):
+    @property
+    def helpstring(self):
+        return self[1]._element.get("helpstring")
+
+    def __repr__(self):
+        items = [f"Chapter {self.helpstring}\n\n"]
+        items.extend([str(item) for item in self._content])
+        return "".join(items)
+
+
+class Section1(Element):
+
+    pass
+
+
+class ProductName(Element):
+
+    pass
+
+
+class MAPDLCommand(Element):
+    """MAPDL command from documentation."""
+
+    def __init__(
+        self,
+        filename,
+        terms,
+        docu_global,
+        version_variables,
+        links,
+        fcache,
+        meta_only=False,
+    ):
+        """Parse command from xml file."""
+        self._xml_filename = filename
+        self._terms = terms
+        self._docu_global = docu_global
+        self._autogenerated_directory_name = version_variables.autogenerated_directory_name
+        self._links = links
+        self._base_url = version_variables.base_url
+        self._fcache = fcache
+        root = fromstring(open(filename, "rb").read())
+
+        # ensure that refentry exists
+        try:
+            self._refentry = next(root.iterfind(".//refentry"))
+        except StopIteration:
+            raise RuntimeError("Not a command file")
+
+        # parse the command
+        super().__init__(self._refentry, parse_children=not meta_only)
+
+    @property
+    def xml_filename(self):
+        """Return the source filename of the command."""
+        return self._xml_filename
+
+    @property
+    def args(self):
+        """Return the command args."""
+        return self._refname_div.refname.args
+
+    @property
+    def default(self):
+        if self._refsynopsis:
+            for item in self._refsynopsis:
+                if str(item.title).strip() == "Command Default":
+                    return item
+
+    @property
+    def arg_desc(self):
+        """Return each argument's description."""
+        refsyn = self.rec_find("Refsynopsisdiv")
+
+        # search by ID
+        if refsyn is None:
+            for elem in self.find_all("RefSection"):
+                if elem.id is not None and "argdescript" in elem.id:
+                    return elem
+
+        return refsyn
+
+    @property
+    def short_desc(self):
+        """Return the short description of the command."""
+        if self._refname_div is not None:
+            return self._refname_div.purpose.to_rst(links=self._links, base_url=self._base_url)
+        return ""
+
+    @property
+    def _metadata(self):
+        if self.rec_find("RefMeta") is None:
+            for item in self._refentry.getchildren():
+                if item.tag == "refmeta":
+                    return parse_element(item)
+        return self.rec_find("RefMeta")
+
+    @property
+    def name(self):
+        return self._metadata.refentry_title
+
+    @property
+    def py_name(self):
+        """Python compatible name."""
+        return to_py_name(self.name)
+
+    @property
+    def py_args(self):
+        return [arg.lower() for arg in self.args]
+
+    @property
+    def py_signature(self):
+        args = ["self"]
+        kwargs = [f'{arg}=""' for arg in self.py_args if "--" not in arg]
+        arg_sig = ", ".join(args + kwargs)
+        return f"def {self.py_name}({arg_sig}):"
+
+    @property
+    def py_docstring(self):
+        """Return the python docstring of this MAPDL command."""
+        apdl_cmd = f"{self._terms['pn006p']} Command: `{self.name} <{self.url}>`_"
+
+        items = [self.short_desc, "", apdl_cmd]
+
+        if self.default is not None:
+            if self.default.tag in item_needing_links_base_url:
+                items += [""] + textwrap.wrap(
+                    "Default: " + self.default.to_rst(links=self._links, base_url=self._base_url)
+                )
+            else:
+                items += [""] + textwrap.wrap("Default: " + self.default.to_rst())
+        if self.args is not None:
+            items += [""] + self.py_parm
+        if self.notes is not None:
+            items += [""] + self.py_notes
+        docstr = "\n".join(items)
+
+        # final post-processing
+        def replacer(match):
+            return match.group().replace("*", r"\*")
+
+        # sphinx doesn't like asterisk symbols
+        docstr = re.sub(r"(?<=\S)\*|(\*\S)", replacer, docstr)
+
+        for key, value in CONST.items():
+            docstr = docstr.replace(key, value)
+        for key, value in CLEANUP.items():
+            docstr = docstr.replace(key, value)
+
+        # delete trailing whitespace for each line
+        docstr = "\n".join(line.rstrip() for line in docstr.splitlines())
+
+        # sub MAPDL command args
+        def arg_replacer(match):
+            text = match.group().replace("`", "")
+            func = re.findall(r"<CMD>(.*)</CMD>", text)[0]
+            arg_str = text.split("</CMD>")[1].strip()[1:]
+            if arg_str.endswith(","):
+                arg_str = arg_str[:-1]
+            if arg_str.startswith(","):
+                arg_str = arg_str[1:]
+
+            split_args = [arg.strip() for arg in arg_str.split(",")]
+
+            tail = ""
+            py_args = []
+            if split_args[-1] == "or":
+                # these are options and not args. Use the first and ignore the rest
+                tail = text.split(split_args[0])[-1]
+                split_args = [split_args[0]]
+
+                for arg in split_args:
+                    if is_numeric(arg):
+                        py_args.append(f"{arg}")
+                    else:
+                        py_args.append(f'"{arg}"')
+            py_args = ", ".join(py_args)
+
+            return f":func:`{func}({py_args}) <{self._autogenerated_directory_name}.{func}>` {tail}"
+
+        # command replacement with args
+        docstr = re.sub(
+            r"<CMD>[a-z0-9]*</CMD>?(\s*,+\s*[A-Za-z0-9%`]*)+",
+            arg_replacer,
+            docstr,
+        )
+
+        def cmd_replacer(match):
+            func = match.group().replace("<CMD>", "").replace("</CMD>", "")
+            return f":func:`{func}() <{self._autogenerated_directory_name}.{func}>`"
+
+        # command replacement without args
+        docstr = re.sub(r"<CMD>[a-z0-9]*</CMD>", cmd_replacer, docstr)
+
+        def pipe_replacer(match):
+            return match.group().replace("|", "\|")
+
+        docstr = re.sub(r"\|(.*)\|", pipe_replacer, docstr)
+
+        def trail_replacer(match):
+            return match.group().replace("_", "")
+
+        docstr = re.sub(r"([^_|^`][A-Za-z0-9]*)_\s", trail_replacer, docstr)
+
+        def term_replacer(match):
+            term = match.group()[1:-1]
+            if term in self._docu_global:
+                _, key, cite_title = self._docu_global[term]
+                if key in self._links:
+                    root_name, root_title, href, text = self._links[key]
+                    link = f"{self._base_url}{root_name}/{href}"
+                    link_text = self._terms.get(cite_title, root_title)
+                    return f"`{link_text} <{link}>`_"
+            else:
+                if term not in self._terms:
+                    warnings.warn(f"term {term} not in terms")
+                    return ""
+                return self._terms[term]
+
+        docstr = re.sub(r"&[\S]*?;", term_replacer, docstr)
+
+        # final line by line cleanup
+        lines = []
+        for line in docstr.splitlines():
+            if "cannot be accessed from a menu" in line:
+                continue
+            if "Graphical picking is available only" in line:
+                continue
+            lines.append(line)
+
+        # ensure the hierarchy of the titles
+        is_equal_sign = False
+        is_dash_sign = False
+        i = 0
+        while i < len(lines):
+            if lines[i].lstrip().startswith("-"):
+                if is_dash_sign == False:
+                    is_dash_sign = True
+            elif lines[i].lstrip().startswith("="):
+                if is_equal_sign or is_dash_sign:
+                    lines[i - 1] = "**" + lines[i - 1] + "**"
+                    # print("après : ", lines[i-1])
+                    lines.pop(i)
+                if is_equal_sign == False:
+                    is_equal_sign = True
+
+            i += 1
+
+        # ensure that lists begin with list-table
+        i = 2
+        while i < len(lines):
+            header = lines[i - 2].lstrip().startswith(":header-rows:") or lines[
+                i - 2
+            ].lstrip().startswith(".. flat-table")
+            corp = lines[i - 1].lstrip().startswith("* -") or lines[i - 1].lstrip().startswith("-")
+            if (lines[i].lstrip().startswith("* -")) and not (corp or header):
+                lines.insert(i, ".. flat-table ::")
+                lines.insert(i + 1, "")
+            i += 1
+
+        # ensure that lists end with a blank line
+        i = 0
+        while i < len(lines):
+            j = 1
+            if lines[i].lstrip().startswith("* -"):
+                while i + j < len(lines) - 1 and lines[i + j].lstrip().startswith("-"):
+                    j += 1
+                if not lines[i + j].lstrip().startswith("* -"):
+                    if i + j == len(lines) - 1:
+                        j += 1
+                    lines.insert(i + j, "")
+            i += j
+
+        # ensure that two similar links are not in a similar file.
+        i = 0
+        link = []
+        flat_table = False
+        while i < len(lines):
+            n_quotes = lines[i].count("`")
+            if ".. flat-table::" in lines[i]:
+                flat_table = True
+            if flat_table is True:
+                if n_quotes > 0:
+                    j = 0
+                    quote = 0
+                    begin = False
+                    while quote < n_quotes and j < len(lines[i]):
+                        if lines[i][j] == "`":
+                            if begin == False:
+                                begin = True
+                                link_i = []
+                                quote += 1
+                            else:
+                                link.append(link_i)
+                                begin = False
+                                quote += 1
+                        else:
+                            if begin == True:
+                                link_i.append(lines[i][j])
+                        j += 1
+            i += 1
+
+        link_list = []
+        for l in link:
+            if "<" in "".join(l):
+                link_list.append("".join(l))
+
+        if len(link_list) > 1:
+            diff_links = set(link_list)
+            if len(diff_links) != len(link_list):
+                for l in diff_links:
+                    name_link = l[: l.index("<") - 1]
+                    nb_l = link_list.count(l)
+                    if nb_l > 1:
+                        first = 0
+                        flat_table = False
+                        for i in range(len(lines)):
+                            if ".. flat-table::" in lines[i]:
+                                flat_table = True
+                            if flat_table is True:
+                                if lines[i].count(l) > 0:
+                                    if first == 0:
+                                        first = 1
+                                    else:
+                                        print("REPLACE", lines[i])
+                                        lines[i] = lines[i].replace(l, name_link)
+
+        docstr = "\n".join(lines)
+
+        # remove repeated line breaks
+        while "\n\n\n" in docstr:
+            docstr = docstr.replace("\n\n\n", "\n\n")
+
+        docstr = re.sub(r"bgcolor=\S\S\S\S\S\S\S\S\S\S?", "", docstr)
+        docstr = re.sub(r"_cellfont Shading=\S\S\S\S\S\S\S\S", "", docstr)
+
+        return docstr
+
+    @property
+    def py_notes(self):
+        """Return the python formatted notes string."""
+        lines = ["Notes", "-" * 5]
+        if self.notes.tag in item_needing_all:
+            lines.append(
+                self.notes.to_rst(
+                    links=self._links,
+                    base_url=self._base_url,
+                    fcache=self._fcache,
+                )
+            )
+        elif self.notes.tag in item_needing_links_base_url:
+            lines.append(self.notes.to_rst(links=self._links, base_url=self._base_url))
+        elif self.notes.tag in item_needing_fcache:
+            lines.append(self.notes.to_rst(links=self._links, fcache=self._fcache))
+        else:
+            lines.append(self.notes.to_rst())
+
+        return lines
+
+    @property
+    def py_parm(self):
+        """Return the python parameters string."""
+        if self.arg_desc is not None:
+            lines = ["Parameters", "-" * 10]
+            for item in self.arg_desc:
+                if not isinstance(item, Variablelist):
+                    if isinstance(item, Element) and "Command Default" in str(item.title):
+                        continue
+                if isinstance(item, Element):
+                    if item.tag in item_needing_all:
+                        lines.append(
+                            item.to_rst(
+                                links=self._links,
+                                base_url=self._base_url,
+                                fcache=self._fcache,
+                            )
+                        )
+                    elif item.tag in item_needing_links_base_url:
+                        lines.append(item.to_rst(links=self._links, base_url=self._base_url))
+                    elif item.tag in item_needing_fcache:
+                        lines.append(item.to_rst(fcache=self._fcache))
+                    else:
+                        lines.append(item.to_rst())
+                else:
+                    lines.append(str(item))
+            return lines
+        return []
+
+    @property
+    def url(self):
+        """Return the URL to the Ansys command documentation."""
+        cmd_base_url = f"{self._base_url}/ans_cmd/"
+        return f"{cmd_base_url}{self.filename}"
+
+    @property
+    def filename(self):
+        """Command filename"""
+        return self[0]._element.get("filename")
+
+    @property
+    def _refname_div(self):
+        return self.rec_find("Refnamediv", self._terms)
+
+    @property
+    def _refsynopsis(self):
+        return self.find("Refsynopsisdiv")
+
+    @property
+    def notes(self):
+        for item in self:
+            if str(item.title).strip() == "Notes":
+                return item
+
+    def __repr__(self):
+        lines = [f"APDL Command: {self.name}"]
+        lines.append("")
+        lines.append("Short Description:")
+        lines.append(f"{self.short_desc}")
+        lines.append("")
+        lines.append("Function signature:")
+        lines.append(", ".join([f"{self.name}"] + self.args))
+        lines.append("")
+        lines.append(str(self.arg_desc))
+        lines.append("")
+        lines.append(str(self.notes))
+
+        return "\n".join(lines)
+
+    @property
+    def py_source(self):
+        """Return the python source"""
+        return textwrap.indent("pass\n", prefix=" " * 4)
+
+    def to_python(self, prefix=""):
+        docstr = textwrap.indent(f'\nr"""{self.py_docstring}\n"""', prefix=prefix + " " * 4)
+        return f"{self.py_signature}{docstr}\n{self.py_source}"
+
+
+class InformalTable(Element):
+    def to_rst(self, prefix=""):
+        return "InformalTables need to be added"
+
+
+parsers = {
+    "sect1": Section1,
+    "chapter": Chapter,
+    "productname": ProductName,
+    "ulink": ULink,
+    "segtitle": SegTitle,
+    "citetitle": CiteTitle,
+    "literallayout": LiteralLayout,
+    "remark": Remark,
+    "tbody": TBody,
+    "entry": Entry,
+    "row": Row,
+    "thead": THead,
+    "colspec": ColSpec,
+    "tgroup": TGroup,
+    "envar": Envar,
+    "term": Term,
+    "varlistentry": VarlistEntry,
+    "imagedata": ImageData,
+    "imageobject": Imageobject,
+    "informalfigure": Informalfigure,
+    "classname": ClassName,
+    "classname": ClassName,
+    "computeroutput": ComputerOutput,
+    "figure": Figure,
+    "footnote": Footnote,
+    "footnoteref": Footnoteref,
+    "formalpara": Formalpara,
+    "glossterm": Glossterm,
+    "guibutton": Guibutton,
+    "guiicon": Guiicon,
+    "highlights": Highlights,
+    "important": Important,
+    "informalequation": Informalequation,
+    "interface": Interface,
+    "markup": Markup,
+    "mediaobject": Mediaobject,
+    "option": Option,
+    "refsect3": RefSection,
+    "refsynopsisdiv": Refsynopsisdiv,
+    "sidebar": Sidebar,
+    "type": XMLType,
+    "warning": XMLWarning,
+    "refsect1": RefSection,
+    "application": Application,
+    "refclass": Refclass,
+    "refpurpose": Refpurpose,
+    "refname": Refname,
+    "refnamediv": Refnamediv,
+    "refentrytitle": Refentrytitle,
+    "primary": Primary,
+    "indexterm": IndexTerm,
+    "refmeta": RefMeta,
+    "function": Function,
+    "refsect2": RefSection,
+    "informalexample": InformalExample,
+    "graphic": Graphic,
+    "caution": Caution,
+    "literal": Literal,
+    "userinput": UserInput,
+    "screen": Screen,
+    "xref": XRef,
+    "subscript": SubScript,
+    "inlinegraphic": InlineGraphic,
+    "math": Math,
+    "inlineequation": InlineEquation,
+    "member": Member,
+    "simplelist": SimpleList,
+    "link": Link,
+    "quote": Quote,
+    "guimenuitem": GuiMenuItem,
+    "itemizedlist": ItemizedList,
+    "listitem": ListItem,
+    "olink": OLink,
+    "replaceable": Replaceable,
+    "emphasis": Emphasis,
+    "example": Example,
+    "command": Command,
+    "title": Title,
+    "para": Paragraph,
+    "table": Table,
+    "orderedlist": OrderedList,
+    "variablelist": Variablelist,
+    "bridgehead": parse_text,
+    "informaltable": InformalTable,
+    "blockquote": BlockQuote,
+    "note": Note,
+    "programlisting": ProgramListing,
+    "guimenu": GuiMenu,
+    "phrase": Phrase,
+    "structname": Structname,
+    "filename": FileName,
+    "superscript": SuperScript,
+    "guilabel": GuiLabel,
+    "code": Code,
+}
+
+item_needing_links_base_url = {
+    "table": Table,
+    "simplelist": SimpleList,
+    "variablelist": Variablelist,
+    "varlistentry": VarlistEntry,
+    "refsect2": RefSection,
+    "refsect3": RefSection,
+    "link": Link,
+    "itemizedlist": ItemizedList,
+    "olink": OLink,
+    "phrase": Phrase,
+    "listitem": ListItem,
+    "emphasis": Emphasis,
+    "formalpara": Formalpara,
+    "orderedlist": OrderedList,
+    "note": Note,
+    "sidebar": Sidebar,
+    "title": Title,
+    "blockquote": BlockQuote,
+    "term": Term,
+    "para": Paragraph,
+    "refsect1": RefSection,
+    "member": Member,
+    "highlights": Highlights,
+    "important": Important,
+    "footnote": Footnote,
+}
+
+item_needing_fcache = {
+    "graphic": Graphic,
+    "figure": Figure,
+    "term": Term,
+    "para": Paragraph,
+    "refsect1": RefSection,
+    "refsect2": RefSection,
+    "refsect3": RefSection,
+    "simplelist": SimpleList,
+    "itemizedlist": ItemizedList,
+    "listitem": ListItem,
+    "informalfigure": Informalfigure,
+    "variablelist": Variablelist,
+    "blockquote": BlockQuote,
+    "informalequation": Informalequation,
+}
+
+item_needing_all = {
+    "itemizedlist": ItemizedList,
+    "variablelist": Variablelist,
+    "simplelist": SimpleList,
+    "term": Term,
+    "para": Paragraph,
+    "refsect1": RefSection,
+    "refsect2": RefSection,
+    "refsect3": RefSection,
+    "listitem": ListItem,
+    "varlistentry": VarlistEntry,
+    "blockquote": BlockQuote,
+}
+
+
+def get_parser():
+    return parsers
+
+
+class OxygenXmlTree(Element):
+    """Load an XML file represented as an AST."""
+
+    def __init__(self, filename, meta_only=False):
+        """Parse command from xml file."""
+        self._xml_filename = filename
+        root = fromstring(open(filename, "rb").read())
+
+        # parse the file
+        super().__init__(root, parse_children=not meta_only)
+
+    @property
+    def xml_filename(self):
+        """Return the source filename of the command."""
+        return self._xml_filename
diff --git a/src/ansys/dita/ast/directory_format.py b/src/ansys/dita/ast/directory_format.py
new file mode 100644
index 000000000..c605007f1
--- /dev/null
+++ b/src/ansys/dita/ast/directory_format.py
@@ -0,0 +1,141 @@
+import argparse
+import os
+
+# Pre-defined directory format
+
+# XML_directory/
+# │
+# ├── graphics/
+# │   ├── .gifs files
+# │   └── images files
+# |
+# ├── links/
+# │   └── .db files
+# |
+# ├── terms/
+# │   ├── glb/
+# │   │   ├── variable_file         #default value is build_variables.ent
+# │   │   ├── global_terms_file     #default value is terms_global.ent
+# │   │   └── manual_file           #default value is manuals.ent
+# │   └── character_directory/         #default value is ent/
+# │       └── .ent files
+# └── xml/
+#     ├── subdirectorys/
+#     │   ├── .xml files
+#     │   └── mathgraphics_directory/   #this is a defalut value
+#     │       └── .svg files
+#     ├── .xml files
+#     └── .ent files
+
+
+def xml_path(path=None):
+    """Return the path to the directory containing the XML documentation.
+    It is advised to follow the pre-defined directory structure.
+
+    Parameters
+    ----------
+    path: str
+        If path is None, the path is set with the argument parser `-p` or `--xml-path`,
+        or with the XML_PATH environment variable.
+
+    Return
+    ------
+    path: str
+        Path of the XML documentation to be converted.
+
+    """
+    if path is None:
+        try:
+            parser = argparse.ArgumentParser()
+            parser.add_argument("--xml-path", "-p", help="XML Documentation path")
+            args = parser.parse_args()
+            path = args.xml_path
+        except:
+            try:
+                path = os.environ.get("XML_PATH")
+            except:
+                pass
+
+    if path is None:
+        raise RuntimeError(
+            "Missing the XML documentation path. Specify this with either --xml-path, -p, or set the XML_PATH environment variable"  # noqa : E501
+        )
+
+    path = os.path.abspath(os.path.expanduser(path))
+
+    # Verification
+    if not os.path.isdir(path):
+        raise FileNotFoundError(f"Documentation path at {path} does not exist")
+
+    return path
+
+
+def get_paths(path, graph_path=None, link_path=None, term_path=None, xml_path=None):
+    """Return the path to the directory containing the graphics.
+
+    Parameters
+    ----------
+    path : strg
+        Path to the directory with the pre-defined format .
+
+    graph_path : strg
+        If not following the XML pre-defined directory format, specify the path to the directory
+        containing the graphics.
+
+    link_path : strg
+        If not following the XML pre-defined directory format, specify the path to the directory
+        containing the links.
+
+    term_path : strg
+        If not following the XML pre-defined directory format, specify the path to the directory
+        containing the terms
+
+    xml_path : strg
+        If not following the XML pre-defined directory format, specify the path to the directory
+        containing the xml.
+
+    Returns
+    -------
+    graphic_path : str
+        Path of the directory containing the graphics.
+
+    link_path : str
+        Path of the directory containing the links.
+
+    term_path : str
+        Path of the directory containing the terms.
+
+    xml_path : str
+        Path of the directory containing the xml.
+
+    """
+
+    if graph_path is None:
+        graph_path = os.path.join(path, "graphics")
+        if not os.path.isdir(graph_path):
+            print(
+                f"WARNING: the path {graph_path} does not exits. Please follow the pre-defined format or enter the graphic path manually."  # noqa : E501
+            )
+
+    if link_path is None:
+        link_path = os.path.join(path, "links")
+        if not os.path.isdir(link_path):
+            print(
+                f"WARNING: the path {link_path} does not exits. Please follow the pre-defined format or enter the link path manually."  # noqa : E501
+            )
+
+    if term_path is None:
+        term_path = os.path.join(path, "terms")
+        if not os.path.isdir(term_path):
+            print(
+                f"WARNING: the path {term_path} does not exits. Please follow the pre-defined format or enter the term path manually."  # noqa : E501
+            )
+
+    if xml_path is None:
+        xml_path = os.path.join(path, "xml")
+        if not os.path.isdir(xml_path):
+            print(
+                f"WARNING: the path {xml_path} does not exits. Please follow the pre-defined format or enter the xml path manually."  # noqa : E501
+            )
+
+    return graph_path, link_path, term_path, xml_path
diff --git a/src/ansys/dita/ast/load_xml_doc.py b/src/ansys/dita/ast/load_xml_doc.py
new file mode 100644
index 000000000..6285c0b35
--- /dev/null
+++ b/src/ansys/dita/ast/load_xml_doc.py
@@ -0,0 +1,316 @@
+import glob
+import os
+from pathlib import Path
+import re
+import unicodedata
+
+import ansys.dita.ast.ast_tree as ast
+import ansys.dita.ast.version_variables as var
+from lxml.etree import ParserError
+from lxml.html import fromstring
+from tqdm import tqdm
+
+
+def load_links(link_path):
+    """Load all links.
+
+    Parameters
+    ----------
+    link_path: strg
+        Path to the links directory.
+
+    Return
+    ------
+    links: dic
+        Dictionary containing the link names and the needed information to render the links.
+
+    """
+
+    db_path = os.path.join(link_path, "*.db")
+    linkmap_fnames = list(glob.glob(db_path, recursive=True))
+    links = {}
+
+    for filename in tqdm(linkmap_fnames, desc="Loading links"):
+        try:
+            linkmap = ast.Element(fromstring(open(filename, "rb").read()))
+        except ParserError:
+            continue
+
+        # toplevel
+        root_name = Path(filename).with_suffix("").name
+        root_title = str(linkmap[0])
+
+        def grab_links(linkmap):
+            for item in linkmap:
+                if not isinstance(item, ast.Element):
+                    continue
+
+                if item.has_children():
+                    grab_links(item)
+
+                href = item.get("href")
+                targetptr = linkmap.get("targetptr")
+                if targetptr is not None and href is not None:
+                    text = ""
+                    if linkmap[0].tag == "ttl":
+                        text = str(linkmap[0])
+                    links[f"{targetptr}"] = (root_name, root_title, href, text)
+
+        grab_links(linkmap)
+
+    return links
+
+
+def load_fcache(graph_path):
+    """Load all graphics and cache the basename without extension.
+
+    Parameters
+    ----------
+    graph_path: strg
+        Path to the graphic directory.
+
+    Return
+    ------
+    fcache: dic
+        Dictionary containing the basenames of the graphics and their path.
+
+    """
+
+    filenames = list(glob.glob(os.path.join(graph_path, "*"), recursive=True))
+    fcache = {}
+    for filename in filenames:
+        basename = Path(filename).with_suffix("").name
+        if not os.path.isfile(filename):
+            raise FileNotFoundError(f"Unable to locate {basename}")
+        fcache[basename] = os.path.split(filename)[-1]
+
+    return fcache
+
+
+def load_docu_global(term_path):
+    """Load all the global documents.
+
+    Parameters
+    ----------
+    term_path: strg
+        Path to the terms directory.
+
+    Return
+    ------
+    docu_global: dic
+        Dictionary containing the entity names from the documentation and their path.
+
+    """
+
+    docu_ent = os.path.join(term_path, "glb", "docu_global.ent")
+
+    docu_global = {}
+    with open(docu_ent, "r") as fid:
+        lines = fid.read().splitlines()
+
+        # have to write our own interperter here since this is non-standard lxml
+        for line in lines:
+            entity_names = re.findall(r"!ENTITY (\S*) ", line)
+            if len(entity_names):
+                entity_name = entity_names[0]
+
+                targetdocs = re.findall(r'targetdoc="(\S*)"', line)
+                targetdoc = targetdocs[0] if len(targetdocs) else None
+
+                targetptrs = re.findall(r'targetptr="(\S*)"', line)
+                targetptr = targetptrs[0] if len(targetptrs) else None
+
+                citetitles = re.findall(r"<citetitle>&(\S*);</citetitle>", line)
+                citetitle = citetitles[0] if len(citetitles) else None
+
+                docu_global[entity_name] = (targetdoc, targetptr, citetitle)
+
+    return docu_global
+
+
+def load_terms(
+    term_path,
+    docu_global,
+    links,
+    fcache,
+    variable_file="build_variables.ent",
+    global_terms_file="terms_global.ent",
+    manual_file="manuals.ent",
+    character_directory="ent",
+):
+
+    """Load all the needed terms.
+
+    Parameters
+    ----------
+    term_path: strg
+        Path to the terms directory.
+
+    docu_global: dic
+        Dictionary containing the entity names from the documentation and their path.
+
+    links: dic
+        Dictionary containing the link names and the needed information to render the links.
+
+    fcache: dic
+        Dictionary containing the basenames of the graphics and their path.
+
+    variable_file: str
+        Name of the file containing the variable terms to be imported.
+        The default value is "build_variables.ent".
+
+    global_terms_file: str
+        Name of the file containing the global terms to be imported.
+        The default value is "terms_global.ent".
+
+    manual_file: strg
+        Name of the file containing the manual entities to be imported.
+        The default value is "manuals.ent".
+
+    character_directory: str
+        Name of the directory containg the entities for the special characters.
+        The default value is "ent.
+
+    Return
+    ------
+    terms: dic
+        Dictionary containing the entity names and their values.
+
+    """
+
+    terms = {}
+
+    variable_path = os.path.join(term_path, "glb", variable_file)
+    if os.path.isfile(variable_path):
+        with open(variable_path, "r") as fid:
+            lines = fid.read().splitlines()
+
+        # have to write our own interperter here since this is non-standard lxml
+        for line in lines:
+            entity_names = re.findall(r"!ENTITY (\S*) ", line)
+            if len(entity_names):
+                matches = re.findall(r"'(\S*)'", line)
+                if len(matches):
+                    terms[entity_names[0]] = matches[0]
+
+    else:
+        print("WARNING: No file founded to define the variable terms.")
+        # This is done manually. To be improved.
+        terms["ansys_internal_version"] = "23.2"
+
+    version_variables = var.Autogenerateddirectory(terms)
+    base_url = version_variables.base_url
+
+    global_terms_path = os.path.join(term_path, "glb", global_terms_file)
+    if os.path.isfile(global_terms_path):
+        with open(global_terms_path, "r") as fid:
+            lines = fid.read().splitlines()
+
+            for line in lines:
+                entity_names = re.findall(r"!ENTITY (\S*) ", line)
+                if len(entity_names):
+                    entity_name = entity_names[0]
+
+                    text = re.findall(r"'(.*)'", line)
+                    if len(text):
+                        terms[entity_name] = text[0]
+    else:
+        print("WARNING: No file founded to define the global terms.")
+
+    # Manually adding terms value from warnings.
+    terms["sgr"] = ":math:`\sigma`"
+    terms["gt"] = ":math:`\sigma`"
+    terms["thgr"] = ":math:`<`"
+    terms["phgr"] = ":math:`<`"
+    terms["ngr"] = ":math:`\phi`"
+    terms["agr"] = ":math:`\alpha`"
+    terms["OHgr"] = ":math:`\Omega`"
+    terms["phis"] = ":math:`\phi`"
+    terms["thetas"] = ":math:`\theta`"
+
+    # These are supposed to be uploaded automatically from the `character.ent` file
+    terms["#13"] = "#13"
+    terms["#160"] = "nbsp"
+    terms["#215"] = "times"
+    terms["#934"] = ":math:`\Phi`"
+
+    # load manuals
+    manual_path = os.path.join(term_path, "glb", manual_file)
+    if os.path.isfile(manual_path):
+        with open(manual_path, "r") as fid:
+            text = fid.read()
+            matches = re.findall(r"ENTITY([\S\s]*?)<!", text)
+            for match in matches:
+                item = ast.Element(fromstring(match)).to_rst(
+                    links=links, base_url=base_url, fcache=fcache
+                )
+                key = item.split()[0]
+                text = item.replace(key, "").strip()
+                if not text.startswith("'"):
+                    continue
+
+                text = text[1:-2].strip()
+
+                def term_replacer(match):
+                    term = match.group()[1:-1]
+                    if term in docu_global:
+                        _, key, cite_title = docu_global[term]
+                        if key in links:
+                            root_name, root_title, href, text = links[key]
+                            link = f"{base_url}{root_name}/{href}"
+                            link_text = terms.get(cite_title, root_title)
+                            return f"`{link_text} <{link}>`_"
+                    else:
+                        if term not in terms:
+                            return match.group()
+                        return terms[term]
+
+                # term_replacer_ = term_replacer(match, links)
+                text = re.sub(r"&[\S]*;", term_replacer, text)
+
+                terms[key] = text
+    else:
+        print("WARNING: No file founded to define the terms from the manual.")
+
+    # load special characters
+    ent_dir = os.path.join(term_path, "ent")
+    if os.path.isdir(ent_dir):
+        isoams_dat = list(glob.glob(os.path.join(ent_dir, "*.ent")))
+        for filename in isoams_dat:
+            with open(filename, "r") as fid:
+                lines = fid.read().splitlines()
+                # have to write our own interperter here since this is non-standard lxml
+                for line in lines:
+                    entity_names = re.findall(r"!ENTITY (\S*) ", line)
+                    if len(entity_names):
+                        matches = re.findall(r"<!--(.*)-->", line)
+                        if len(matches):
+                            char_name = matches[0].strip()
+                            try:
+                                terms[entity_names[0]] = unicodedata.lookup(char_name)
+                            except KeyError:
+                                continue
+
+    else:
+        print("WARNING: No entitiy directory.")
+
+    # This is not working for now, to be improved
+
+    # filename = os.path.join(
+    #     doc_path, "DITA-Open-Toolkit/lib/xerces-2_11_0.AnsysCustom/docs/dtd/", "characters.ent"
+    # )
+    # with open(filename, "r") as fid:
+    #     lines = fid.read().splitlines()
+    #     # have to write our own interperter here since this is non-standard lxml
+    #     for line in lines:
+    #         entity_names = re.findall(r"!ENTITY (\S*)", line)
+    #         if len(entity_names):
+    #             matches = re.findall(r"#\d\d\d", line)
+    #             if len(matches):
+    #                 char_name = matches[0]
+    #                 try:
+    #                     terms[entity_names[0]] = char_name
+    #                 except KeyError:
+    #                     continue
+
+    return terms, version_variables
diff --git a/src/ansys/dita/ast/version_variables.py b/src/ansys/dita/ast/version_variables.py
new file mode 100644
index 000000000..46e9d80b9
--- /dev/null
+++ b/src/ansys/dita/ast/version_variables.py
@@ -0,0 +1,40 @@
+class Autogenerateddirectory:
+    def __init__(self, terms) -> None:
+        self.autogenerated_directory_name = "ansys.dita.generatedcommands"
+        self.version = terms["ansys_internal_version"]
+        self.base_url = f"https://ansyshelp.ansys.com/Views/Secured/corp/v{self._version}/en/"
+        self.cmd_base_url = f"{self.base_url}/ans_cmd/"
+
+    @property
+    def autogenerated_directory_name(self):
+        return self._autogenerated_directory_name
+
+    @property
+    def version(self):
+        return self._version
+
+    @property
+    def base_url(self):
+        return self._base_url
+
+    @property
+    def cmd_base_url(self):
+        return self._cmd_base_url
+
+    @autogenerated_directory_name.setter
+    def autogenerated_directory_name(self, directory_name):
+        self._autogenerated_directory_name = directory_name
+
+    @version.setter
+    def version(self, version_value):
+        self._version = version_value
+        self._base_url = f"https://ansyshelp.ansys.com/Views/Secured/corp/v{self._version}/en/"
+
+    @base_url.setter
+    def base_url(self, url):
+        self._base_url = url
+        self._cmd_base_url = f"{self._base_url}/ans_cmd/"
+
+    @cmd_base_url.setter
+    def cmd_base_url(self, base_url):
+        self._cmd_base_url = base_url
diff --git a/src/ansys/dita/ast/writer.py b/src/ansys/dita/ast/writer.py
new file mode 100644
index 000000000..c288a1e16
--- /dev/null
+++ b/src/ansys/dita/ast/writer.py
@@ -0,0 +1,283 @@
+import glob
+import os
+import shutil
+
+from ansys.dita.ast import ast_tree as ast
+from ansys.dita.ast import directory_format as path
+from ansys.dita.ast import load_xml_doc as load
+from tqdm import tqdm
+
+generated_src_code = os.path.join("src", "ansys", "dita", "generatedcommands")
+
+# map APDL command to pymapdl function
+CMD_MAP = {}
+
+# common statements used within the docs to avoid duplication
+CONST = {
+    "Dtl?": "",
+    "Caret?": "",
+    "Caret1?": "",
+    "Caret 40?": "",
+    '``"``': "``",
+}
+
+# APDL commands to skip
+SKIP_APDL = {"*IF", "*ELSE", "C***", "*RETURN", "*DEL"}
+SKIP_PYMAPDL = {"if", "else", "c", "return", "del"}
+
+
+def nested_exec(text):
+    """Nested execute."""
+    exec(text)
+
+
+def convert(directory_path, command=None):
+    """Covert an XML directory into an RST one."""
+
+    graph_path, link_path, term_path, xml_path = path.get_paths(directory_path)
+    links = load.load_links(link_path)
+    fcache = load.load_fcache(graph_path)
+    docu_global = load.load_docu_global(term_path)
+    terms, version_variables = load.load_terms(term_path, docu_global, links, fcache)
+
+    def load_commands(
+        xml_path,
+        meta_only=False,
+    ):
+        """Scrape the command info from the MAPDL XML command reference.
+
+        Parameters
+        ----------
+        xml_path : str
+            Path to the directory containing the XML files to be converted.
+
+        Examples
+        --------
+        >>> from convert import load_commands
+        >>> commands = load_commands(
+        ...     '/home/user/source/mapdl-cmd-doc/docu_files/ans_cmd/'
+        ... )
+
+        """
+        if not os.path.isdir(xml_path):
+            raise FileNotFoundError(f'Invalid path "{xml_path}"')
+
+        filenames = list(glob.glob(os.path.join(xml_path, "**", "*.xml"), recursive=True))
+
+        if meta_only:
+            desc = "Loading command metadata"
+        else:
+            desc = "Loading commands"
+
+        mapdl_commands = []
+        for filename in tqdm(filenames, desc=desc):
+            try:
+                mapdl_commands.append(
+                    ast.MAPDLCommand(
+                        filename,
+                        terms,
+                        docu_global,
+                        version_variables,
+                        links,
+                        fcache,
+                        meta_only=meta_only,
+                    )
+                )
+            except RuntimeError:
+                continue
+
+        return {cmd.name: cmd for cmd in mapdl_commands}
+
+    command_meta = load_commands(
+        os.path.expanduser(xml_path),
+        meta_only=True,
+    )
+    command_names = command_meta.keys()
+
+    # create command mapping between the ansys command name and the pymapdl method.
+    # remove the start and slash whenever possible, for example, /GCOLUMN can simply
+    # be gcolumn since it's the only command, but VGET and *VGET must be vget and star_vget
+
+    # convert all to flat and determine number of occurances
+    proc_names = []
+    for cmd_name in command_names:
+        cmd_name = cmd_name.lower()
+        if not cmd_name[0].isalnum():
+            cmd_name = cmd_name[1:]
+        proc_names.append(cmd_name)
+
+    # reserved command mapping
+    COMMAND_MAPPING = {"*DEL": "stardel"}
+
+    # second pass for each name
+    for ans_name in command_names:
+        if ans_name in COMMAND_MAPPING:
+            py_name = COMMAND_MAPPING[ans_name]
+        else:
+            lower_name = ans_name.lower()
+            if not lower_name[0].isalnum():
+                alpha_name = lower_name[1:]
+            else:
+                alpha_name = lower_name
+
+            if proc_names.count(alpha_name) != 1:
+                py_name = lower_name.replace("/", "slash").replace("*", "star")
+            else:
+                py_name = alpha_name
+
+        CMD_MAP[ans_name] = py_name
+
+    # TODO : accept conversion of a single command
+
+    # convert a single command
+    # if command is not None:
+    #     if command not in command_meta:
+    #         raise ValueError(f"Invalid command {command}")
+    #     fname = command_meta[command].xml_filename
+    #     cmd = ast.MAPDLCommand(os.path.expanduser(fname), )
+    #     commands = {to_py_name(cmd.name): cmd}
+    # else:  # convert all commands
+
+    commands = load_commands(xml_path)
+
+    return commands
+
+
+def copy_package(template_path, new_package_path, clean=False):
+    """Add files and directory from a template directory path to a new path.
+
+    Parameters
+    ----------
+    template_path : str
+        Path containing the directory to be copied.
+
+    new_package_path : str
+        Path containing the directory where the new files and directorys will be added to.
+
+    clean : Bool
+        Whether the directorys in new_package_path need to be cleared before adding new files
+        or not. Default to False.
+
+    Returns
+    -------
+    str
+        Path containing the source files of the created
+        ``ansys-mapdl-commands`` package.
+
+    """
+
+    for filename in glob.glob(os.path.join(template_path, "*"), recursive=True):
+        split_name_dir = filename.split(os.path.sep)
+        new_path_dir = os.path.join(new_package_path, split_name_dir[-1])
+
+        if os.path.isdir(filename):
+            if not os.path.isdir(new_path_dir):
+                os.makedirs(new_path_dir, exist_ok=True)
+            elif os.path.isdir(new_path_dir) and clean:
+                shutil.rmtree(new_path_dir)
+                os.makedirs(new_path_dir)
+            copy_package(filename, new_path_dir, clean)
+
+        else:
+            shutil.copy(filename, new_package_path)
+
+
+def write_source(commands, path, new_package_path=None, clean=True):
+    """Write out MAPDL commands as Python source files.
+
+    Parameters
+    ----------
+    commands : list[MAPDLCommand]
+        List of MAPDLCommand.
+
+    path : str
+        Path containing ``_package`` directory.
+
+    new_package_path : str
+        Path where to copy the ``_package`` directory. Default is ``./package``.
+
+    Returns
+    -------
+    str
+        Path containing the source files of the created
+        ``ansys-mapdl-commands`` package.
+
+    """
+    template_path = os.path.join(path, "_package")
+    if not os.path.isdir(template_path):
+        raise FileNotFoundError(
+            f"Unable to locate the package templates path at '{template_path}'. "
+            f"Expected the _package directory at '{path}'."
+        )
+
+    if new_package_path is None:
+        new_package_path = os.path.join(path, "package")
+
+    if clean:
+        if os.path.isdir(new_package_path):
+            shutil.rmtree(new_package_path)
+
+    cmd_path = os.path.join(new_package_path, generated_src_code)
+    if not os.path.isdir(cmd_path):
+        os.makedirs(cmd_path)
+
+    for ans_name, cmd_obj in tqdm(commands.items(), desc="Writing commands"):
+        if ans_name in SKIP_APDL:
+            continue
+        cmd_name = ast.to_py_name(ans_name)
+        path = os.path.join(cmd_path, f"{cmd_name}.py")
+        with open(path, "w", encoding="utf-8") as fid:
+            fid.write(cmd_obj.to_python())
+
+        try:
+            nested_exec(cmd_obj.to_python())
+        except:
+            raise RuntimeError(f"Failed to execute {cmd_name}.py") from None
+
+    mod_file = os.path.join(cmd_path, "__init__.py")
+    with open(mod_file, "w") as fid:
+        for ans_name in commands:
+            if ans_name in SKIP_APDL:
+                continue
+            cmd_name = ast.to_py_name(ans_name)
+            fid.write(f"from .{cmd_name} import *\n")
+
+    print(f"Commands written to {cmd_path}")
+
+    # copy package files to the package directory
+    copy_package(template_path, new_package_path, clean)
+
+    return cmd_path
+
+
+def write_docs(commands, path):
+    """Output to the tinypages directory.
+
+    Parameters
+    ----------
+    path : str
+        Path to the new doc pages directory.
+
+    """
+
+    package_path = os.path.join(path, "package")
+    doc_package_path = os.path.join(package_path, "doc/source")
+    if not os.path.isdir(doc_package_path):
+        os.makedirs(doc_package_path)
+
+    doc_src = os.path.join(doc_package_path, "docs.rst")
+    with open(doc_src, "w") as fid:
+        fid.write("###########\n")
+        fid.write("Autosummary\n")
+        fid.write("###########\n")
+
+        fid.write(".. currentmodule:: ansys.dita.generatedcommands\n\n")
+        fid.write(".. autosummary::\n")
+        fid.write("   :template: base.rst\n")
+        fid.write("   :toctree: _autosummary/\n\n")
+        for ans_name in commands:
+            if ans_name not in SKIP_APDL:
+                cmd_name = ast.to_py_name(ans_name)
+                fid.write(f"   {cmd_name}\n")
+
+    return doc_src
diff --git a/xml2rst.py b/xml2rst.py
new file mode 100644
index 000000000..922eb0e52
--- /dev/null
+++ b/xml2rst.py
@@ -0,0 +1,40 @@
+from ansys.dita.ast import writer as wr
+
+"""
+Parse MAPDL command XML documentation.
+
+"""
+import argparse
+import os
+
+# from pathlib import Path
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--xml-path", "-p", help="Documentation path")
+    # not accepted
+    # parser.add_argument("--command", help="Convert a single command")
+    parser.add_argument("--no-clean", help="Do not remove the existing package directory")
+    parser.add_argument("--no_docs", action="store_true", help="Do not write to tinypages")
+    args = parser.parse_args()
+
+    directory_path = args.xml_path
+    if directory_path is None:
+        directory_path = os.environ.get("XML_PATH")
+    if directory_path is None:
+        raise RuntimeError(
+            "Missing the XML documentation path. Specify this with either --xml-path, -p, or set the XML_PATH environment variable"  # noqa : E501
+        )
+
+    directory_path = os.path.abspath(os.path.expanduser(directory_path))
+    cur_path = os.getcwd()
+
+    # Verification
+    if not os.path.isdir(directory_path):
+        raise FileNotFoundError(f"Documentation path at {directory_path} does not exist")
+
+    # chap2.write_chapt2(doc_path, conv_path, links, base_url)
+
+    commands = wr.convert(directory_path)
+    cmd_path = wr.write_source(commands, cur_path)
+    doc_src = wr.write_docs(commands, cur_path)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 000000000..bf066ae6b
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,84 @@
+import os
+
+import ansys.dita.ast.directory_format as ff
+import ansys.dita.ast.load_xml_doc as lxd
+import ansys.dita.ast.writer as wrt
+import pytest
+
+pytest_plugins = ["pytester"]
+
+# This is to run in the GH actions.
+def pytest_addoption(parser):
+    parser.addoption("--ghdir", action="store", default="./")
+
+
+def pytest_generate_tests(metafunc):
+    # This is called for every test. Only get/set command line arguments
+    # if the argument is specified in the list of test "fixturenames".
+    option_value = metafunc.config.option.ghdir
+    if "ghdir" in metafunc.fixturenames and option_value is not None:
+        metafunc.parametrize("ghdir", [option_value])
+
+
+@pytest.fixture
+def directory_path(ghdir):
+    if os.environ.get("ON_CI", "").lower() == "true":
+        directory_path = os.path.join(ghdir, "mapdl-cmd-doc")
+    else:
+        directory_path = os.path.abspath(os.path.join(os.getcwd(), "../mapdl-cmd-doc-generalized"))
+    return directory_path
+
+
+@pytest.fixture
+def graph_path(directory_path):
+    return ff.get_paths(directory_path)[0]
+
+
+@pytest.fixture
+def link_path(directory_path):
+    return ff.get_paths(directory_path)[1]
+
+
+@pytest.fixture
+def term_path(directory_path):
+    return ff.get_paths(directory_path)[2]
+
+
+@pytest.fixture
+def xml_path(directory_path):
+    return ff.get_paths(directory_path)[3]
+
+
+@pytest.fixture
+def docu_global(term_path):
+    return lxd.load_docu_global(term_path)
+
+
+@pytest.fixture
+def links(link_path):
+    return lxd.load_links(link_path)
+
+
+@pytest.fixture
+def fcache(graph_path):
+    return lxd.load_fcache(graph_path)
+
+
+@pytest.fixture
+def load_terms(term_path, docu_global, links, fcache):
+    return lxd.load_terms(term_path, docu_global, links, fcache)
+
+
+@pytest.fixture
+def terms(load_terms):
+    return load_terms[0]
+
+
+@pytest.fixture
+def version_variables(load_terms):
+    return load_terms[1]
+
+
+@pytest.fixture
+def commands(directory_path):
+    return wrt.convert(directory_path)
diff --git a/tests/test_directory_format.py b/tests/test_directory_format.py
new file mode 100644
index 000000000..e8a6b88fe
--- /dev/null
+++ b/tests/test_directory_format.py
@@ -0,0 +1,20 @@
+import os
+
+import ansys.dita.ast.directory_format as ff
+import pytest
+
+
+def test_xml_path(directory_path):
+    assert os.path.abspath(os.path.expanduser(directory_path)) == ff.xml_path(directory_path)
+
+
+def test_missing_xml_path():
+    with pytest.raises(RuntimeError):
+        ff.xml_path()
+
+
+def test_get_path(directory_path):
+    assert os.path.join(directory_path, "graphics") == ff.get_paths(directory_path)[0]
+    assert os.path.join(directory_path, "links") == ff.get_paths(directory_path)[1]
+    assert os.path.join(directory_path, "terms") == ff.get_paths(directory_path)[2]
+    assert os.path.join(directory_path, "xml") == ff.get_paths(directory_path)[3]
diff --git a/tests/test_load_xml_doc.py b/tests/test_load_xml_doc.py
new file mode 100644
index 000000000..69e2442f3
--- /dev/null
+++ b/tests/test_load_xml_doc.py
@@ -0,0 +1,28 @@
+import ansys.dita.ast.load_xml_doc as lxd
+
+
+def test_load_links(link_path):
+    links = lxd.load_links(link_path)
+    assert links["ds_Support_Types"] == (
+        "wb_sim",
+        "",
+        "ds_Elastic_Support.html",
+        "Support Type Boundary Conditions",
+    )
+
+
+def test_load_fcache(graph_path):
+    fcache = lxd.load_fcache(graph_path)
+    assert fcache["gcmdrsymm4"] == "gcmdrsymm4.png"
+
+
+def test_load_docu_global(term_path):
+    docu_global = lxd.load_docu_global(term_path)
+    assert docu_global["acpmdug"] == ("acp_md", "acp_md", "bk_acp_md")
+
+
+def test_load_terms(load_terms):
+    terms, version_variables = load_terms
+    assert terms["me"] == "Ansys Mechanical"
+    assert version_variables.autogenerated_directory_name == "ansys.dita.generatedcommands"
+    assert version_variables.version == terms["ansys_internal_version"]
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
deleted file mode 100644
index 861ebf3b1..000000000
--- a/tests/test_metadata.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from ansys.dita.ast import __version__
-
-
-def test_pkg_version():
-    assert __version__ == "0.1.dev0"
diff --git a/tests/test_writer.py b/tests/test_writer.py
new file mode 100644
index 000000000..f83468246
--- /dev/null
+++ b/tests/test_writer.py
@@ -0,0 +1,63 @@
+import os
+import shutil
+
+import ansys.dita.ast.writer as wrt
+import pytest
+
+
+def test_convert(commands):
+    assert commands["/XFRM"].name == "/XFRM"
+    assert (
+        "APDL Command: WRITE\n\nShort Description:\nWrites the radiation matrix file.\n\nFunction signature:\nWRITE,"  # noqa : E501
+        in commands["WRITE"].__repr__()
+    )
+    assert commands["E"].py_source == "    pass\n"
+    assert (
+        'def zoom(self, wn="", lab="", x1="", y1="", x2="", y2=""):\n    r"""Zooms a region of a display window.\n\n'  # noqa : E501
+        in commands["/ZOOM"].to_python()
+    )
+
+
+def test_copy_package():
+    cwd = os.getcwd()
+    new_package_path = os.path.join(cwd, "tmp_directory")
+    if os.path.isdir(new_package_path):
+        shutil.rmtree(new_package_path)
+    os.makedirs(new_package_path)
+    template_path = os.path.join(cwd, "_package")
+    wrt.copy_package(template_path, new_package_path)
+    assert os.path.isdir(new_package_path) is True
+    assert os.path.isdir(os.path.join(new_package_path, "doc")) is True
+    assert os.path.isfile(os.path.join(new_package_path, "LICENSE")) is True
+    assert os.path.isdir(os.path.join(new_package_path, "doc", "source", "_templates")) is True
+    assert (
+        os.path.isfile(os.path.join(new_package_path, "doc", "source", "_templates", "base.rst"))
+        is True
+    )
+    shutil.rmtree(new_package_path)
+
+
+@pytest.fixture
+def cwd():
+    return os.getcwd()
+
+
+@pytest.fixture
+def package_path(cwd):
+    return os.path.join(cwd, "package")
+
+
+def test_write_source(commands, cwd, package_path):
+    cmd_path = wrt.write_source(commands, cwd)
+    assert cmd_path == os.path.join(package_path, wrt.generated_src_code)
+    assert os.path.isfile(os.path.join(cmd_path, "acel.py"))
+
+
+def test_write_docs(commands):
+    cwd = os.getcwd()
+    doc_src = wrt.write_docs(commands, cwd)
+    file = open(doc_src, "r")
+    content = file.read()
+    file.close()
+    assert "Autosummary" in content
+    assert "agen" in content
diff --git a/tests/test_xml_ast.py b/tests/test_xml_ast.py
new file mode 100644
index 000000000..978602230
--- /dev/null
+++ b/tests/test_xml_ast.py
@@ -0,0 +1,91 @@
+# import os
+
+import ansys.dita.ast.ast_tree as ast
+from lxml.html import fromstring
+import pytest
+
+
+@pytest.mark.parametrize(
+    "command,expected_output", [("/PREP7", "prep7"), ("*DMAT", "dmat"), ("SORT", "sort")]
+)
+def test_py_name(command, expected_output):
+    assert ast.to_py_name(command) == expected_output
+
+
+@pytest.fixture
+def alpha_text():
+    return "This is a test."
+
+
+def test_trail_alpha(alpha_text):
+    split_alpha = ast.split_trail_alpha(alpha_text)
+    assert split_alpha == ("This ", "is a test.")
+
+
+@pytest.mark.parametrize(
+    "numeric_string,expected_output", [("2804", True), ("28.04", True), ("TEA", False)]
+)
+def test_is_numeric(numeric_string, expected_output):
+    assert ast.is_numeric(numeric_string) == expected_output
+
+
+@pytest.fixture
+def str_element():
+    return "<element extra_value='0000'>element test</element>"
+
+
+@pytest.fixture
+def element(str_element):
+    element = fromstring(str_element)
+    return element
+
+
+def test_init_Element(element):
+    elem = ast.Element(element)
+    assert elem._element == element
+    assert elem._content == ["element test"]
+
+
+def test_parse_element(element):
+    element_parser = ast.parse_element(element)
+    ast_element = ast.Element(element)
+    assert len(element_parser) == len(ast_element)
+    assert type(element_parser) == type(ast_element)
+    assert element_parser._element == ast_element._element
+    assert element_parser._content == ast_element._content
+
+
+@pytest.fixture
+def str_element_with_children():
+    return '<element extra_value="0000">element test with children <child>child 1</child> <child>child 2</child> <child>child 3</child></element>'  # noqa : E501
+
+
+@pytest.fixture
+def element_with_children(str_element_with_children):
+    return fromstring(str_element_with_children)
+
+
+@pytest.fixture
+def Element_with_children(element_with_children):
+    return ast.Element(element_with_children)
+
+
+def test_parse_children(element_with_children):
+    children_fromparse = ast.parse_children(element_with_children)
+    children_fromget = element_with_children.getchildren()
+    assert children_fromparse == children_fromget
+
+
+def test_init_Element_with_children(element_with_children, Element_with_children):
+    assert Element_with_children._element == element_with_children
+    assert len(Element_with_children._content) == 4
+
+
+def test_Element_text_content(Element_with_children):
+    content = Element_with_children.text_content
+    assert content == "element test with children child 1 child 2 child 3"
+
+
+def test_Element_raw(str_element_with_children, Element_with_children):
+    raw = Element_with_children.raw
+    assert raw == str_element_with_children