Repository: almenscorner/IntuneCD (language: python)

Issue:
Markdown formatting changes
Some markdown visualisation libraries (MkDocs in my case) don't render the table properly if there's no blank line or heading directly before the table. In this case, if a config item has no assignments, the table is inserted on the line directly below the description which causes it to be displayed as raw text rather than a table.

This PR adds a `### Configuration` heading before all `config_table`. Alternatively the heading could be replaced with a simple newline.

Additionally, when using some special characters in descriptions, they can have unforeseen effects on the rendered markdown. If all special characters are escaped, then it removes the risk of this happening. Possibly this is overkill and completely unnecessary, I can remove it if you think so.


Discussion / hints:

Fix patch:
diff --git a/setup.cfg b/setup.cfg
index 5736eec8..4668e496 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = IntuneCD
-version = 1.4.3
+version = 1.4.4
 author = Tobias Almén
 author_email = almenscorner@outlook.com
 description = Tool to backup and update configurations in Intune
diff --git a/src/IntuneCD/documentation_functions.py b/src/IntuneCD/documentation_functions.py
index 84f76ca4..49fcb2c3 100644
--- a/src/IntuneCD/documentation_functions.py
+++ b/src/IntuneCD/documentation_functions.py
@@ -42,6 +42,20 @@ def write_table(data):
     return writer
 
 
+def escape_markdown(text):
+    """
+    This function escapes markdown characters.
+
+    :param text: The text to be escaped
+    :return: The escaped text
+    """
+
+    # Escape markdown characters
+    parse = re.sub(r"([\_*\[\]()\{\}`>\#\+\-=|\.!])", r"\\\1", text)
+
+    return parse
+
+
 def assignment_table(data):
     """
     This function creates the Markdown assignments table.
@@ -297,10 +311,11 @@ def document_configs(configpath, outpath, header, max_length, split, cleanup):
                     if "name" in repo_data:
                         md.write("## " + repo_data["name"] + "\n")
                     if description:
-                        md.write(f"Description: {description} \n")
+                        md.write(f"Description: {escape_markdown(description)} \n")
                     if assignments_table:
                         md.write("### Assignments \n")
                         md.write(str(assignments_table) + "\n")
+                    md.write('### Configuration \n')
                     md.write(str(config_table) + "\n")
 
 
@@ -402,10 +417,11 @@ def document_management_intents(configpath, outpath, header, split):
                     if "name" in repo_data:
                         md.write("## " + repo_data["name"] + "\n")
                     if description:
-                        md.write(f"Description: {description} \n")
+                        md.write(f"Description: {escape_markdown(description)} \n")
                     if assignments_table:
                         md.write("### Assignments \n")
                         md.write(str(assignments_table) + "\n")
+                    md.write('### Configuration \n')
                     md.write(str(config_table) + "\n")
 
 


Test patch:
diff --git a/tests/test_documentation_functions.py b/tests/test_documentation_functions.py
index 464840c6..e3b68f59 100644
--- a/tests/test_documentation_functions.py
+++ b/tests/test_documentation_functions.py
@@ -7,6 +7,7 @@
     write_table,
     assignment_table,
     remove_characters,
+    escape_markdown,
     clean_list,
     document_configs,
     document_management_intents,
@@ -76,6 +77,12 @@ def test_remove_characters(self):
 
         self.assertEqual(remove_characters(self.string), "test")
 
+    def test_escape_markdown(self):
+        """The escaped string should be returned."""
+        self.string = "\\`*_{}[]()#+-.!Hello World"
+
+        self.assertEqual(escape_markdown(self.string), "\\\\`\\*\\_\\{\\}\\[\\]\\(\\)\\#\\+\\-\\.\\!Hello World")
+
     def test_clean_list_list(self):
         """The list should be returned."""
         self.list = [{"teamIdentifier": "test", "bundleId": "test"}]
@@ -107,7 +114,7 @@ def test_document_configs(self):
             '{"@odata.type":"test","test":"test","name":"test","description":"test","testvals":"1,2","testbool":false,"testlist":["test"],"testlistdict":[{"test":{"test":{"test":["1"]}}}],"testdict2":{"test":{"test":{"test":["1"]}}},"testdictlist":{"test":["a","b","c"]},"assignments":[{"intent":"apply","target":{"@odata.type":"#test","groupName":"test-group","deviceAndAppManagementAssignmentFilterId":"test-filter","deviceAndAppManagementAssignmentFilterType":"test"}}]}',
             encoding="utf-8",
         )
-        self.expected_data = "#test##testDescription:test###Assignments|intent|target|filtertype|filtername||------|----------|-----------|-----------||apply|test-group|test|test-filter||setting|value||------------|-------------------------------------------------------||Odatatype|test||Test|test||Name|test||Testvals|1,2||Testbool|False||Testlist|test<br/>||Testlistdict|**test:**<ul>**test:**<ul><li>1</li></ul></ul><br/>||Testdict2|**test**<ul>**test:**<ul><li>1</li></ul><br/></ul>||Testdictlist|**test:**<ul><li>a</li><li>b</li><li>c</li></ul>|"
+        self.expected_data = "#test##testDescription:test###Assignments|intent|target|filtertype|filtername||------|----------|-----------|-----------||apply|test-group|test|test-filter|###Configuration|setting|value||------------|-------------------------------------------------------||Odatatype|test||Test|test||Name|test||Testvals|1,2||Testbool|False||Testlist|test<br/>||Testlistdict|**test:**<ul>**test:**<ul><li>1</li></ul></ul><br/>||Testdict2|**test**<ul>**test:**<ul><li>1</li></ul><br/></ul>||Testdictlist|**test:**<ul><li>a</li><li>b</li><li>c</li></ul>|"
 
         document_configs(
             f"{self.directory.path}/config", f"{self.directory.path}/test.md", "test", 100, split=False, cleanup=True
@@ -126,7 +133,7 @@ def test_document_management_intents(self):
             '{"test": "test", "name": "test", "description": "test", "settingsDelta": [{"test": "test", "definitionId": "deviceConfiguration--macOSEndpointProtectionConfiguration_fileVaultNumberOfTimesUserCanIgnore","valueJson": "1","value": 1}], "assignments": [{"intent": "apply", "target": {"@odata.type": "#test", "groupName": "test-group", "deviceAndAppManagementAssignmentFilterId": "test-filter", "deviceAndAppManagementAssignmentFilterType": "test"}}]}',
             encoding="utf-8",
         )
-        self.expected_data = "#intent##testDescription:test###Assignments|intent|target|filtertype|filtername||------|----------|-----------|-----------||apply|test-group|test|test-filter||setting|value||------------------------------------------|-----||Test|test||Name|test||FileVaultNumberOfTimesUserCanIgnore|1|"
+        self.expected_data = "#intent##testDescription:test###Assignments|intent|target|filtertype|filtername||------|----------|-----------|-----------||apply|test-group|test|test-filter|###Configuration|setting|value||------------------------------------------|-----||Test|test||Name|test||FileVaultNumberOfTimesUserCanIgnore|1|"
 
         document_management_intents(f"{self.directory.path}/intent/", f"{self.directory.path}/test.md", "intent", split=False)
         with open(f"{self.directory.path}/test.md", "r") as f: