Repository: haidaraM/ansible-playbook-grapher (language: python)

Issue:
[Feature request] generate mermaid instead of images
**Is your feature request related to a problem? Please describe.**
No

**Describe the solution you'd like**
It would be nice to be able to generate mermaid instead of SVG/images, so we could embed the output directly into places that support it , like on GitHub.

quick example:

```mermaid
graph TD
    A[Thievery] -->|Get money| B(Go shopping)
    B --> C{Let me think}
    C -->|One| D[Laptop]
    C -->|Two| E[iPhone]
    C -->|Three| F[fa:fa-car Car]
```


Discussion / hints:
I actually have this on my mind since a few months but was wondering if it would be useful or not for users. 

Will see what I can do. Should be doable now that the rendering part is separated from the parser. 

Fix patch:
diff --git a/.gitignore b/.gitignore
index c778053d..f93e3323 100644
--- a/.gitignore
+++ b/.gitignore
@@ -109,4 +109,5 @@ venv.bak/
 .vagrant
 Vagrantfile
 generated-svgs
+generated-mermaids
 **/.DS_Store
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fb024dd..2fe9eebc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,24 @@
+# 2.0.0 (unreleased)
+
+## What's Changed
+
+- Add support for MermaidJS. See https://github.com/haidaraM/ansible-playbook-grapher/issues/137
+- Update various Dependencies: pytest, pytest-cov, ansible-core, pyquery etc...
+- ci: Add dependabot for github-actions
+- Rename some tests files
+- ...
+
+## Breaking changes
+
+This version contains the following breaking changes. Some of them may likely affect you if you were using the grapher
+as a library inside another project:
+
+- Completely refactor the rendering part of the part by making it more extensible in order to support Mermaid. 
+- Fill the plays, blocks and node with color to make them more visible in the output
+- Rename the file `graph.py` to `graph_model.py`
+- Use the concatenation of the playbook names as the output filename when graphing multiple playbooks instead of the
+  first playbook.
+
 # 1.2.0 (2022-08-21)
 
 ## What's Changed
diff --git a/Makefile b/Makefile
index 29f09ae7..3d9a45e3 100644
--- a/Makefile
+++ b/Makefile
@@ -23,10 +23,10 @@ test_install: build
 	@./tests/test_install.sh $(VIRTUALENV_DIR) $(ANSIBLE_VERSION)
 
 test:
-	cd tests && pytest
+	cd tests && pytest test_cli.py test_utils.py test_parser.py test_graph_model.py test_graphviz_postprocessor.py test_graphviz_renderer.py test_mermaid_renderer.py
 
 clean:
 	@echo "Cleaning..."
-	rm -rf ansible_playbook_grapher.egg-info build dist $(VIRTUALENV_DIR) tests/htmlcov tests/.pytest_cache .eggs tests/generated-svgs tests/.coverage
+	rm -rf ansible_playbook_grapher.egg-info build dist $(VIRTUALENV_DIR) tests/htmlcov tests/.pytest_cache .eggs tests/generated-* tests/.coverage
 
 .PHONY: clean test_install
\ No newline at end of file
diff --git a/README.md b/README.md
index c0612e89..5299082f 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ JavaScript:
   since `ansible` [package has been split](https://www.ansible.com/blog/ansible-3.0.0-qa)).
 - **Graphviz**: The tool used to generate the graph in SVG.
   ```shell script
-  $ sudo apt-get install graphviz # or yum install or brew install
+  sudo apt-get install graphviz # or yum install or brew install
   ```
 
 I try to respect [Red Hat Ansible Engine Life Cycle](https://access.redhat.com/support/policy/updates/ansible-engine)
@@ -51,6 +51,19 @@ for the supported Ansible version.
 pip install ansible-playbook-grapher
 ```
 
+### Renderers
+
+At the time of writing, two renderers are supported:
+
+1. `graphviz` (default): Generate the graph in SVG. It has more features and is more tested: open protocol,
+   highlight linked nodes...
+2. `mermaid-flowchart`: Generate the graph in [Mermaid](https://mermaid.js.org/syntax/flowchart.html) format. You can
+   directly embed the graph in your markdown and GitHub (
+   and [other integrations](https://mermaid.js.org/ecosystem/integrations.html)) will render it. **Early support**.
+
+If you are interested to support more renderers, feel free to create an issue or raise a PR based on the existing
+renderers.
+
 ## Usage
 
 ```shell
@@ -71,6 +84,305 @@ ansible-playbook-grapher tests/fixtures/with_block.yml
 
 ![Example](https://raw.githubusercontent.com/haidaraM/ansible-playbook-grapher/main/img/block.png)
 
+```bash
+ansible-playbook-grapher --include-role-tasks --renderer mermaid-flowchart tests/fixtures/multi-plays.yml tests/fixtures/with_block.yml
+```
+
+```mermaid
+---
+title: Ansible Playbook Grapher
+---
+%%{ init: { 'flowchart': { 'curve': 'bumpX' } } }%%
+flowchart LR
+	%% Start of the playbook 'tests/fixtures/multi-plays.yml'
+	playbook_bd8798bc("tests/fixtures/multi-plays.yml")
+		%% Start of the play 'Play: all (0)'
+		play_fb11c1f8["Play: all (0)"]
+		style play_fb11c1f8 fill:#4d8b41,color:#ffffff
+		playbook_bd8798bc --> |"1"| play_fb11c1f8
+		linkStyle 0 stroke:#4d8b41,color:#4d8b41
+			pre_task_0aea2a6b["[pre_task] Pretask"]
+			style pre_task_0aea2a6b stroke:#4d8b41,fill:#ffffff
+			play_fb11c1f8 --> |"1"| pre_task_0aea2a6b
+			linkStyle 1 stroke:#4d8b41,color:#4d8b41
+			pre_task_e48d82b5["[pre_task] Pretask 2"]
+			style pre_task_e48d82b5 stroke:#4d8b41,fill:#ffffff
+			play_fb11c1f8 --> |"2"| pre_task_e48d82b5
+			linkStyle 2 stroke:#4d8b41,color:#4d8b41
+			%% Start of the role 'fake_role'
+			role_d95e9d5f("[role] fake_role")
+			style role_d95e9d5f fill:#4d8b41,color:#ffffff,stroke:#4d8b41
+			play_fb11c1f8 --> |"3"| role_d95e9d5f
+			linkStyle 3 stroke:#4d8b41,color:#4d8b41
+				task_41f6dd12["fake_role : Debug 1"]
+				style task_41f6dd12 stroke:#4d8b41,fill:#ffffff
+				role_d95e9d5f --> |"1 [when: ansible_distribution == 'Debian']"| task_41f6dd12
+				linkStyle 4 stroke:#4d8b41,color:#4d8b41
+				task_9dcf29d3["fake_role : Debug 2"]
+				style task_9dcf29d3 stroke:#4d8b41,fill:#ffffff
+				role_d95e9d5f --> |"2 [when: ansible_distribution == 'Debian']"| task_9dcf29d3
+				linkStyle 5 stroke:#4d8b41,color:#4d8b41
+				task_dc3f4611["fake_role : Debug 3 with double quote &#34;here&#34; in the name"]
+				style task_dc3f4611 stroke:#4d8b41,fill:#ffffff
+				role_d95e9d5f --> |"3 [when: ansible_distribution == 'Debian']"| task_dc3f4611
+				linkStyle 6 stroke:#4d8b41,color:#4d8b41
+			%% End of the role 'fake_role'
+			%% Start of the role 'display_some_facts'
+			role_2b74c9f5("[role] display_some_facts")
+			style role_2b74c9f5 fill:#4d8b41,color:#ffffff,stroke:#4d8b41
+			play_fb11c1f8 --> |"4"| role_2b74c9f5
+			linkStyle 7 stroke:#4d8b41,color:#4d8b41
+				task_1c0691fb["display_some_facts : ansible_architecture"]
+				style task_1c0691fb stroke:#4d8b41,fill:#ffffff
+				role_2b74c9f5 --> |"1"| task_1c0691fb
+				linkStyle 8 stroke:#4d8b41,color:#4d8b41
+				task_c7d6574e["display_some_facts : ansible_date_time"]
+				style task_c7d6574e stroke:#4d8b41,fill:#ffffff
+				role_2b74c9f5 --> |"2"| task_c7d6574e
+				linkStyle 9 stroke:#4d8b41,color:#4d8b41
+				task_e26456cf["display_some_facts : Specific included task for Debian"]
+				style task_e26456cf stroke:#4d8b41,fill:#ffffff
+				role_2b74c9f5 --> |"3"| task_e26456cf
+				linkStyle 10 stroke:#4d8b41,color:#4d8b41
+			%% End of the role 'display_some_facts'
+			task_f0ec5674["[task] Add backport {{backport}}"]
+			style task_f0ec5674 stroke:#4d8b41,fill:#ffffff
+			play_fb11c1f8 --> |"5"| task_f0ec5674
+			linkStyle 11 stroke:#4d8b41,color:#4d8b41
+			task_614fa7f3["[task] Install packages"]
+			style task_614fa7f3 stroke:#4d8b41,fill:#ffffff
+			play_fb11c1f8 --> |"6"| task_614fa7f3
+			linkStyle 12 stroke:#4d8b41,color:#4d8b41
+			post_task_bfd4a733["[post_task] Posttask"]
+			style post_task_bfd4a733 stroke:#4d8b41,fill:#ffffff
+			play_fb11c1f8 --> |"7"| post_task_bfd4a733
+			linkStyle 13 stroke:#4d8b41,color:#4d8b41
+			post_task_6728f20f["[post_task] Posttask 2"]
+			style post_task_6728f20f stroke:#4d8b41,fill:#ffffff
+			play_fb11c1f8 --> |"8"| post_task_6728f20f
+			linkStyle 14 stroke:#4d8b41,color:#4d8b41
+		%% End of the play 'Play: all (0)'
+		%% Start of the play 'Play: database (0)'
+		play_200a2cda["Play: database (0)"]
+		style play_200a2cda fill:#195db3,color:#ffffff
+		playbook_bd8798bc --> |"2"| play_200a2cda
+		linkStyle 15 stroke:#195db3,color:#195db3
+			%% Start of the role 'fake_role'
+			role_584c099b("[role] fake_role")
+			style role_584c099b fill:#195db3,color:#ffffff,stroke:#195db3
+			play_200a2cda --> |"1"| role_584c099b
+			linkStyle 16 stroke:#195db3,color:#195db3
+				task_4363c0b7["fake_role : Debug 1"]
+				style task_4363c0b7 stroke:#195db3,fill:#ffffff
+				role_584c099b --> |"1 [when: ansible_distribution == 'Debian']"| task_4363c0b7
+				linkStyle 17 stroke:#195db3,color:#195db3
+				task_aff132aa["fake_role : Debug 2"]
+				style task_aff132aa stroke:#195db3,fill:#ffffff
+				role_584c099b --> |"2 [when: ansible_distribution == 'Debian']"| task_aff132aa
+				linkStyle 18 stroke:#195db3,color:#195db3
+				task_69120ebe["fake_role : Debug 3 with double quote &#34;here&#34; in the name"]
+				style task_69120ebe stroke:#195db3,fill:#ffffff
+				role_584c099b --> |"3 [when: ansible_distribution == 'Debian']"| task_69120ebe
+				linkStyle 19 stroke:#195db3,color:#195db3
+			%% End of the role 'fake_role'
+			%% Start of the role 'display_some_facts'
+			role_f46ed679("[role] display_some_facts")
+			style role_f46ed679 fill:#195db3,color:#ffffff,stroke:#195db3
+			play_200a2cda --> |"2"| role_f46ed679
+			linkStyle 20 stroke:#195db3,color:#195db3
+				task_5f4d00e6["display_some_facts : ansible_architecture"]
+				style task_5f4d00e6 stroke:#195db3,fill:#ffffff
+				role_f46ed679 --> |"1"| task_5f4d00e6
+				linkStyle 21 stroke:#195db3,color:#195db3
+				task_fa40d63b["display_some_facts : ansible_date_time"]
+				style task_fa40d63b stroke:#195db3,fill:#ffffff
+				role_f46ed679 --> |"2"| task_fa40d63b
+				linkStyle 22 stroke:#195db3,color:#195db3
+				task_3527696f["display_some_facts : Specific included task for Debian"]
+				style task_3527696f stroke:#195db3,fill:#ffffff
+				role_f46ed679 --> |"3"| task_3527696f
+				linkStyle 23 stroke:#195db3,color:#195db3
+			%% End of the role 'display_some_facts'
+		%% End of the play 'Play: database (0)'
+		%% Start of the play 'Play: webserver (0)'
+		play_34799e65["Play: webserver (0)"]
+		style play_34799e65 fill:#7f4d73,color:#ffffff
+		playbook_bd8798bc --> |"3"| play_34799e65
+		linkStyle 24 stroke:#7f4d73,color:#7f4d73
+			%% Start of the role 'nested_include_role'
+			role_1bfe7836("[role] nested_include_role")
+			style role_1bfe7836 fill:#7f4d73,color:#ffffff,stroke:#7f4d73
+			play_34799e65 --> |"1"| role_1bfe7836
+			linkStyle 25 stroke:#7f4d73,color:#7f4d73
+				task_40d7c36f["nested_include_role : Ensure postgresql is at the latest version"]
+				style task_40d7c36f stroke:#7f4d73,fill:#ffffff
+				role_1bfe7836 --> |"1"| task_40d7c36f
+				linkStyle 26 stroke:#7f4d73,color:#7f4d73
+				task_bc9f916c["nested_include_role : Ensure that postgresql is started"]
+				style task_bc9f916c stroke:#7f4d73,fill:#ffffff
+				role_1bfe7836 --> |"2"| task_bc9f916c
+				linkStyle 27 stroke:#7f4d73,color:#7f4d73
+				%% Start of the role 'display_some_facts'
+				role_dfff70cd("[role] display_some_facts")
+				style role_dfff70cd fill:#7f4d73,color:#ffffff,stroke:#7f4d73
+				role_1bfe7836 --> |"3 [when: x is not defined]"| role_dfff70cd
+				linkStyle 28 stroke:#7f4d73,color:#7f4d73
+					task_2700f9a8["display_some_facts : ansible_architecture"]
+					style task_2700f9a8 stroke:#7f4d73,fill:#ffffff
+					role_dfff70cd --> |"1"| task_2700f9a8
+					linkStyle 29 stroke:#7f4d73,color:#7f4d73
+					task_84bd5d7e["display_some_facts : ansible_date_time"]
+					style task_84bd5d7e stroke:#7f4d73,fill:#ffffff
+					role_dfff70cd --> |"2"| task_84bd5d7e
+					linkStyle 30 stroke:#7f4d73,color:#7f4d73
+					task_1b02d165["display_some_facts : Specific included task for Debian"]
+					style task_1b02d165 stroke:#7f4d73,fill:#ffffff
+					role_dfff70cd --> |"3"| task_1b02d165
+					linkStyle 31 stroke:#7f4d73,color:#7f4d73
+				%% End of the role 'display_some_facts'
+				%% Start of the role 'fake_role'
+				role_6433854b("[role] fake_role")
+				style role_6433854b fill:#7f4d73,color:#ffffff,stroke:#7f4d73
+				role_1bfe7836 --> |"4"| role_6433854b
+				linkStyle 32 stroke:#7f4d73,color:#7f4d73
+					task_10479304["fake_role : Debug 1"]
+					style task_10479304 stroke:#7f4d73,fill:#ffffff
+					role_6433854b --> |"1"| task_10479304
+					linkStyle 33 stroke:#7f4d73,color:#7f4d73
+					task_a13ab280["fake_role : Debug 2"]
+					style task_a13ab280 stroke:#7f4d73,fill:#ffffff
+					role_6433854b --> |"2"| task_a13ab280
+					linkStyle 34 stroke:#7f4d73,color:#7f4d73
+					task_bffa1ed0["fake_role : Debug 3 with double quote &#34;here&#34; in the name"]
+					style task_bffa1ed0 stroke:#7f4d73,fill:#ffffff
+					role_6433854b --> |"3"| task_bffa1ed0
+					linkStyle 35 stroke:#7f4d73,color:#7f4d73
+				%% End of the role 'fake_role'
+			%% End of the role 'nested_include_role'
+			%% Start of the role 'display_some_facts'
+			role_9188508e("[role] display_some_facts")
+			style role_9188508e fill:#7f4d73,color:#ffffff,stroke:#7f4d73
+			play_34799e65 --> |"2"| role_9188508e
+			linkStyle 36 stroke:#7f4d73,color:#7f4d73
+				task_b0401984["display_some_facts : ansible_architecture"]
+				style task_b0401984 stroke:#7f4d73,fill:#ffffff
+				role_9188508e --> |"1"| task_b0401984
+				linkStyle 37 stroke:#7f4d73,color:#7f4d73
+				task_417dd6b4["display_some_facts : ansible_date_time"]
+				style task_417dd6b4 stroke:#7f4d73,fill:#ffffff
+				role_9188508e --> |"2"| task_417dd6b4
+				linkStyle 38 stroke:#7f4d73,color:#7f4d73
+				task_60860d86["display_some_facts : Specific included task for Debian"]
+				style task_60860d86 stroke:#7f4d73,fill:#ffffff
+				role_9188508e --> |"3"| task_60860d86
+				linkStyle 39 stroke:#7f4d73,color:#7f4d73
+			%% End of the role 'display_some_facts'
+		%% End of the play 'Play: webserver (0)'
+	%% End of the playbook 'tests/fixtures/multi-plays.yml'
+
+	%% Start of the playbook 'tests/fixtures/with_block.yml'
+	playbook_2e263466("tests/fixtures/with_block.yml")
+		%% Start of the play 'Play: all (0)'
+		play_74990123["Play: all (0)"]
+		style play_74990123 fill:#a4288c,color:#ffffff
+		playbook_2e263466 --> |"1"| play_74990123
+		linkStyle 40 stroke:#a4288c,color:#a4288c
+			%% Start of the role 'fake_role'
+			role_5914bd45("[role] fake_role")
+			style role_5914bd45 fill:#a4288c,color:#ffffff,stroke:#a4288c
+			play_74990123 --> |"1"| role_5914bd45
+			linkStyle 41 stroke:#a4288c,color:#a4288c
+				pre_task_87f2b15f["fake_role : Debug 1"]
+				style pre_task_87f2b15f stroke:#a4288c,fill:#ffffff
+				role_5914bd45 --> |"1"| pre_task_87f2b15f
+				linkStyle 42 stroke:#a4288c,color:#a4288c
+				pre_task_64224a5c["fake_role : Debug 2"]
+				style pre_task_64224a5c stroke:#a4288c,fill:#ffffff
+				role_5914bd45 --> |"2"| pre_task_64224a5c
+				linkStyle 43 stroke:#a4288c,color:#a4288c
+				pre_task_eaf098d3["fake_role : Debug 3 with double quote &#34;here&#34; in the name"]
+				style pre_task_eaf098d3 stroke:#a4288c,fill:#ffffff
+				role_5914bd45 --> |"3"| pre_task_eaf098d3
+				linkStyle 44 stroke:#a4288c,color:#a4288c
+			%% End of the role 'fake_role'
+			%% Start of the block 'Block in pre task'
+			block_59e01f41["[block] Block in pre task"]
+			style block_59e01f41 fill:#a4288c,color:#ffffff,stroke:#a4288c
+			play_74990123 --> |"2"| block_59e01f41
+			linkStyle 45 stroke:#a4288c,color:#a4288c
+			subgraph subgraph_block_59e01f41["Block in pre task "]
+				pre_task_d56eed17["debug"]
+				style pre_task_d56eed17 stroke:#a4288c,fill:#ffffff
+				block_59e01f41 --> |"1"| pre_task_d56eed17
+				linkStyle 46 stroke:#a4288c,color:#a4288c
+			end
+			%% End of the block 'Block in pre task'
+			task_13ae5b2d["[task] Install tree"]
+			style task_13ae5b2d stroke:#a4288c,fill:#ffffff
+			play_74990123 --> |"3"| task_13ae5b2d
+			linkStyle 47 stroke:#a4288c,color:#a4288c
+			%% Start of the block 'Install Apache'
+			block_58464279["[block] Install Apache"]
+			style block_58464279 fill:#a4288c,color:#ffffff,stroke:#a4288c
+			play_74990123 --> |"4 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| block_58464279
+			linkStyle 48 stroke:#a4288c,color:#a4288c
+			subgraph subgraph_block_58464279["Install Apache "]
+				task_3c6e5034["Install some packages"]
+				style task_3c6e5034 stroke:#a4288c,fill:#ffffff
+				block_58464279 --> |"1 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| task_3c6e5034
+				linkStyle 49 stroke:#a4288c,color:#a4288c
+				task_7f997d4e["template"]
+				style task_7f997d4e stroke:#a4288c,fill:#ffffff
+				block_58464279 --> |"2 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| task_7f997d4e
+				linkStyle 50 stroke:#a4288c,color:#a4288c
+				%% Start of the block ''
+				block_58e72e2c["[block] "]
+				style block_58e72e2c fill:#a4288c,color:#ffffff,stroke:#a4288c
+				block_58464279 --> |"3 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| block_58e72e2c
+				linkStyle 51 stroke:#a4288c,color:#a4288c
+				subgraph subgraph_block_58e72e2c[" "]
+					task_752a43fc["get_url"]
+					style task_752a43fc stroke:#a4288c,fill:#ffffff
+					block_58e72e2c --> |"1 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6') and True]"| task_752a43fc
+					linkStyle 52 stroke:#a4288c,color:#a4288c
+					task_b31070fc["command"]
+					style task_b31070fc stroke:#a4288c,fill:#ffffff
+					block_58e72e2c --> |"2 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| task_b31070fc
+					linkStyle 53 stroke:#a4288c,color:#a4288c
+				end
+				%% End of the block ''
+				task_4ff99c46["service"]
+				style task_4ff99c46 stroke:#a4288c,fill:#ffffff
+				block_58464279 --> |"4 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| task_4ff99c46
+				linkStyle 54 stroke:#a4288c,color:#a4288c
+			end
+			%% End of the block 'Install Apache'
+			task_27e5fe68["[task] Create a username for tomcat"]
+			style task_27e5fe68 stroke:#a4288c,fill:#ffffff
+			play_74990123 --> |"5"| task_27e5fe68
+			linkStyle 55 stroke:#a4288c,color:#a4288c
+			post_task_b09e48f3["[post_task] Debug"]
+			style post_task_b09e48f3 stroke:#a4288c,fill:#ffffff
+			play_74990123 --> |"6"| post_task_b09e48f3
+			linkStyle 56 stroke:#a4288c,color:#a4288c
+			%% Start of the block 'My post task block'
+			block_644a319d["[block] My post task block"]
+			style block_644a319d fill:#a4288c,color:#ffffff,stroke:#a4288c
+			play_74990123 --> |"7"| block_644a319d
+			linkStyle 57 stroke:#a4288c,color:#a4288c
+			subgraph subgraph_block_644a319d["My post task block "]
+				post_task_e6f19df5["template"]
+				style post_task_e6f19df5 stroke:#a4288c,fill:#ffffff
+				block_644a319d --> |"1"| post_task_e6f19df5
+				linkStyle 58 stroke:#a4288c,color:#a4288c
+			end
+			%% End of the block 'My post task block'
+		%% End of the play 'Play: all (0)'
+	%% End of the playbook 'tests/fixtures/with_block.yml'
+
+
+```
+
 Note on block: Since `block`s are logical group of tasks, the conditional `when` is not displayed on the edges pointing
 to them but on the tasks inside the block. This
 mimics [Ansible behavior](https://docs.ansible.com/ansible/latest/user_guide/playbooks_blocks.html#grouping-tasks-with-blocks)
@@ -86,8 +398,12 @@ usage: ansible-playbook-grapher [-h] [-v] [-i INVENTORY]
                                 [-o OUTPUT_FILENAME]
                                 [--open-protocol-handler {default,vscode,custom}]
                                 [--open-protocol-custom-formats OPEN_PROTOCOL_CUSTOM_FORMATS]
-                                [--group-roles-by-name] [--version] [-t TAGS]
-                                [--skip-tags SKIP_TAGS] [--vault-id VAULT_IDS]
+                                [--group-roles-by-name]
+                                [--renderer {graphviz,mermaid-flowchart}]
+                                [--renderer-mermaid-directive RENDERER_MERMAID_DIRECTIVE]
+                                [--renderer-mermaid-orientation {TD,RL,BT,RL,LR}]
+                                [--version] [-t TAGS] [--skip-tags SKIP_TAGS]
+                                [--vault-id VAULT_IDS]
                                 [--ask-vault-password | --vault-password-file VAULT_PASSWORD_FILES]
                                 [-e EXTRA_VARS]
                                 playbooks [playbooks ...]
@@ -97,12 +413,13 @@ Make graphs from your Ansible Playbooks.
 positional arguments:
   playbooks             Playbook(s) to graph
 
-optional arguments:
+options:
   --ask-vault-password, --ask-vault-pass
                         ask for vault password
   --group-roles-by-name
                         When rendering the graph, only a single role will be
-                        display for all roles having the same names.
+                        display for all roles having the same names. Default:
+                        False
   --include-role-tasks  Include the tasks of the role in the graph.
   --open-protocol-custom-formats OPEN_PROTOCOL_CUSTOM_FORMATS
                         The custom formats to use as URLs for the nodes in the
@@ -129,6 +446,16 @@ optional arguments:
                         will be open with VSCode. For 'custom', you need to
                         set a custom format with --open-protocol-custom-
                         formats.
+  --renderer {graphviz,mermaid-flowchart}
+                        The renderer to use to generate the graph. Default:
+                        graphviz
+  --renderer-mermaid-directive RENDERER_MERMAID_DIRECTIVE
+                        The directive for the mermaid renderer. Can be used to
+                        customize the output: fonts, theme, curve etc. More
+                        info at https://mermaid.js.org/config/directives.html.
+                        Default: '%%{ init: { "flowchart": { "curve": "bumpX" } } }%%'
+  --renderer-mermaid-orientation {TD,RL,BT,RL,LR}
+                        The orientation of the flow chart. Default: 'LR'
   --skip-tags SKIP_TAGS
                         only run plays and tasks whose tags do not match these
                         values
@@ -148,7 +475,7 @@ optional arguments:
   -o OUTPUT_FILENAME, --output-file-name OUTPUT_FILENAME
                         Output filename without the '.svg' extension. Default:
                         <playbook>.svg
-  -s, --save-dot-file   Save the dot file used to generate the graph.
+  -s, --save-dot-file   Save the graphviz dot file used to generate the graph.
   -t TAGS, --tags TAGS  only run plays and tasks tagged with these values
   -v, --verbose         Causes Ansible to print more debug messages. Adding
                         multiple -v will increase the verbosity, the builtin
@@ -195,7 +522,7 @@ Run the tests and open the generated files in your system’s default viewer app
 
 ```shell script
 export TEST_VIEW_GENERATED_FILE=1
-$ make test # run all tests
+make test # run all tests
 ```
 
 The graphs are generated in the folder `tests/generated-svgs`. They are also generated as artefacts
diff --git a/ansibleplaybookgrapher/__init__.py b/ansibleplaybookgrapher/__init__.py
index c6c2098f..ac76d0a4 100644
--- a/ansibleplaybookgrapher/__init__.py
+++ b/ansibleplaybookgrapher/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2022 Mohamed El Mouctar HAIDARA
+# Copyright (C) 2023 Mohamed El Mouctar HAIDARA
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -13,18 +13,18 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-from typing import Dict, List, Set
+from typing import Dict, List, Set, Tuple
 
 from ansible.utils.display import Display
 
-from ansibleplaybookgrapher.graph import (
+from ansibleplaybookgrapher.graph_model import (
     PlaybookNode,
     RoleNode,
     PlayNode,
 )
 from ansibleplaybookgrapher.parser import PlaybookParser
 from ansibleplaybookgrapher.utils import merge_dicts
-from .graph import PlaybookNode, PlayNode, TaskNode, RoleNode, BlockNode
+from .graph_model import PlaybookNode, PlayNode, TaskNode, RoleNode, BlockNode
 from .parser import PlaybookParser
 
 __version__ = "2.0.0-dev"
@@ -40,16 +40,13 @@ def __init__(self, playbook_filenames: List[str]):
         """
         self.playbook_filenames = playbook_filenames
 
-        # The usage of the roles in all playbooks
-        self.roles_usage: Dict[RoleNode, Set[PlayNode]] = {}
-
     def parse(
         self,
         include_role_tasks: bool = False,
         tags: List[str] = None,
         skip_tags: List[str] = None,
         group_roles_by_name: bool = False,
-    ) -> List[PlaybookNode]:
+    ) -> Tuple[List[PlaybookNode], Dict[RoleNode, Set[PlayNode]]]:
         """
         Parses all the provided playbooks
         :param include_role_tasks: Should we include the role tasks
@@ -59,6 +56,8 @@ def parse(
         :return:
         """
         playbook_nodes = []
+        roles_usage: Dict[RoleNode, Set[PlayNode]] = {}
+
         for playbook_file in self.playbook_filenames:
             display.display(f"Parsing playbook {playbook_file}")
             playbook_parser = PlaybookParser(
@@ -72,8 +71,6 @@ def parse(
             playbook_nodes.append(playbook_node)
 
             # Update the usage of the roles
-            self.roles_usage = merge_dicts(
-                self.roles_usage, playbook_node.roles_usage()
-            )
+            roles_usage = merge_dicts(roles_usage, playbook_node.roles_usage())
 
-        return playbook_nodes
+        return playbook_nodes, roles_usage
diff --git a/ansibleplaybookgrapher/cli.py b/ansibleplaybookgrapher/cli.py
index a1c3a442..acef4571 100644
--- a/ansibleplaybookgrapher/cli.py
+++ b/ansibleplaybookgrapher/cli.py
@@ -26,6 +26,11 @@
 from ansibleplaybookgrapher import __prog__, __version__, Grapher
 from ansibleplaybookgrapher.renderer import OPEN_PROTOCOL_HANDLERS
 from ansibleplaybookgrapher.renderer.graphviz import GraphvizRenderer
+from ansibleplaybookgrapher.renderer.mermaid import (
+    MermaidFlowChartRenderer,
+    DEFAULT_DIRECTIVE as MERMAID_DEFAULT_DIRECTIVE,
+    DEFAULT_ORIENTATION as MERMAID_DEFAULT_ORIENTATION,
+)
 
 # The display is a singleton. This instruction will NOT return a new instance.
 # We explicitly set the verbosity after the init.
@@ -52,26 +57,41 @@ def run(self):
 
         display.verbosity = self.options.verbosity
         grapher = Grapher(self.options.playbook_filenames)
-        playbook_nodes = grapher.parse(
+        playbook_nodes, roles_usage = grapher.parse(
             include_role_tasks=self.options.include_role_tasks,
             tags=self.options.tags,
             skip_tags=self.options.skip_tags,
             group_roles_by_name=self.options.group_roles_by_name,
         )
-        # TODO: add condition to choose the renderer
-        renderer = GraphvizRenderer(
-            playbook_nodes=playbook_nodes,
-            roles_usage=grapher.roles_usage,
-        )
-        output_path = renderer.render(
-            open_protocol_handler=self.options.open_protocol_handler,
-            open_protocol_custom_formats=self.options.open_protocol_custom_formats,
-            save_dot_file=self.options.save_dot_file,
-            output_filename=self.options.output_filename,
-            view=self.options.view,
-        )
 
-        return output_path
+        if self.options.renderer == "graphviz":
+            renderer = GraphvizRenderer(
+                playbook_nodes=playbook_nodes,
+                roles_usage=roles_usage,
+            )
+            output_path = renderer.render(
+                open_protocol_handler=self.options.open_protocol_handler,
+                open_protocol_custom_formats=self.options.open_protocol_custom_formats,
+                output_filename=self.options.output_filename,
+                view=self.options.view,
+                save_dot_file=self.options.save_dot_file,
+            )
+
+            return output_path
+        else:
+            renderer = MermaidFlowChartRenderer(
+                playbook_nodes=playbook_nodes,
+                roles_usage=roles_usage,
+            )
+            output_path = renderer.render(
+                open_protocol_handler=self.options.open_protocol_handler,
+                open_protocol_custom_formats=self.options.open_protocol_custom_formats,
+                output_filename=self.options.output_filename,
+                view=self.options.view,
+                directive=self.options.renderer_mermaid_directive,
+                orientation=self.options.renderer_mermaid_orientation,
+            )
+            return output_path
 
     def _add_my_options(self):
         """
@@ -102,7 +122,7 @@ def _add_my_options(self):
             dest="save_dot_file",
             action="store_true",
             default=False,
-            help="Save the dot file used to generate the graph.",
+            help="Save the graphviz dot file used to generate the graph.",
         )
 
         self.parser.add_argument(
@@ -155,7 +175,27 @@ def _add_my_options(self):
             "--group-roles-by-name",
             action="store_true",
             default=False,
-            help="When rendering the graph, only a single role will be display for all roles having the same names.",
+            help="When rendering the graph, only a single role will be display for all roles having the same names. Default: %(default)s",
+        )
+
+        self.parser.add_argument(
+            "--renderer",
+            choices=["graphviz", "mermaid-flowchart"],
+            default="graphviz",
+            help="The renderer to use to generate the graph. Default: %(default)s",
+        )
+
+        self.parser.add_argument(
+            "--renderer-mermaid-directive",
+            default=MERMAID_DEFAULT_DIRECTIVE,
+            help="The directive for the mermaid renderer. Can be used to customize the output: fonts, theme, curve etc. More info at https://mermaid.js.org/config/directives.html. Default: '%(default)s'",
+        )
+
+        self.parser.add_argument(
+            "--renderer-mermaid-orientation",
+            default=MERMAID_DEFAULT_ORIENTATION,
+            choices=["TD", "RL", "BT", "RL", "LR"],
+            help="The orientation of the flow chart. Default: '%(default)s'",
         )
 
         self.parser.add_argument(
@@ -192,10 +232,11 @@ def post_process_args(self, options):
         self.options = options
 
         if self.options.output_filename is None:
-            # use the first playbook name (without the extension) as output filename
-            self.options.output_filename = os.path.splitext(
-                ntpath.basename(self.options.playbook_filenames[0])
-            )[0]
+            basenames = map(ntpath.basename, self.options.playbook_filenames)
+            basenames_without_ext = "-".join(
+                [os.path.splitext(basename)[0] for basename in basenames]
+            )
+            self.options.output_filename = basenames_without_ext
 
         if self.options.open_protocol_handler == "custom":
             self.validate_open_protocol_custom_formats()
diff --git a/ansibleplaybookgrapher/graph.py b/ansibleplaybookgrapher/graph_model.py
similarity index 98%
rename from ansibleplaybookgrapher/graph.py
rename to ansibleplaybookgrapher/graph_model.py
index 908c3bc8..2e28f2aa 100644
--- a/ansibleplaybookgrapher/graph.py
+++ b/ansibleplaybookgrapher/graph_model.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2022 Mohamed El Mouctar HAIDARA
+# Copyright (C) 2023 Mohamed El Mouctar HAIDARA
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -14,7 +14,7 @@
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 import os
 from collections import defaultdict
-from typing import Dict, List, Set, Type, Tuple
+from typing import Dict, List, Set, Type, Tuple, Optional
 
 from ansibleplaybookgrapher.utils import generate_id, get_play_colors
 
@@ -31,7 +31,7 @@ def __init__(
         when: str = "",
         raw_object=None,
         parent: "Node" = None,
-        index: int = None,
+        index: Optional[int] = None,
     ):
         """
 
@@ -53,7 +53,7 @@ def __init__(
         self.set_position()
 
         # The index of this node in the parent node if it has one (starting from 1)
-        self.index: int = index
+        self.index: Optional[int] = index
 
     def set_position(self):
         """
diff --git a/ansibleplaybookgrapher/parser.py b/ansibleplaybookgrapher/parser.py
index 4743ec92..f48df70e 100644
--- a/ansibleplaybookgrapher/parser.py
+++ b/ansibleplaybookgrapher/parser.py
@@ -27,7 +27,7 @@
 from ansible.template import Templar
 from ansible.utils.display import Display
 
-from ansibleplaybookgrapher.graph import (
+from ansibleplaybookgrapher.graph_model import (
     TaskNode,
     PlaybookNode,
     RoleNode,
@@ -194,7 +194,9 @@ def parse(self, *args, **kwargs) -> PlaybookNode:
 
             display.v(f"Parsing {play_name}")
 
-            play_node = PlayNode(play_name, hosts=play_hosts, raw_object=play)
+            play_node = PlayNode(
+                play_name, hosts=play_hosts, raw_object=play, parent=playbook_root_node
+            )
             playbook_root_node.add_node("plays", play_node)
 
             # loop through the pre_tasks
@@ -318,8 +320,7 @@ def _include_tasks_in_blocks(
         for task_or_block in block.block:
             if hasattr(task_or_block, "loop") and task_or_block.loop:
                 display.warning(
-                    "Looping on tasks or roles are not supported for the moment. "
-                    f"Only the task having the loop argument will be added to the graph."
+                    "Looping on tasks or roles are not supported for the moment. Only the task having the loop argument will be added to the graph."
                 )
 
             if isinstance(task_or_block, Block):
diff --git a/ansibleplaybookgrapher/renderer/__init__.py b/ansibleplaybookgrapher/renderer/__init__.py
index 7499bb68..4b5d7736 100644
--- a/ansibleplaybookgrapher/renderer/__init__.py
+++ b/ansibleplaybookgrapher/renderer/__init__.py
@@ -17,7 +17,7 @@
 
 from ansible.utils.display import Display
 
-from ansibleplaybookgrapher.graph import (
+from ansibleplaybookgrapher.graph_model import (
     PlaybookNode,
     PlayNode,
     RoleNode,
@@ -41,7 +41,42 @@
 }
 
 
+class Renderer(ABC):
+    def __init__(
+        self,
+        playbook_nodes: PlaybookNode,
+        roles_usage: Dict[RoleNode, Set[PlayNode]],
+    ):
+        self.playbook_nodes = playbook_nodes
+        self.roles_usage = roles_usage
+
+    @abstractmethod
+    def render(
+        self,
+        open_protocol_handler: str,
+        open_protocol_custom_formats: Dict[str, str],
+        output_filename: str,
+        view: bool,
+        **kwargs,
+    ) -> str:
+        """
+        Render the playbooks to a file.
+        :param open_protocol_handler: The protocol handler name to use
+        :param open_protocol_custom_formats: The custom formats to use when the protocol handler is set to custom
+        :param output_filename: without any extension
+        :param view: Whether to open the rendered file in the default viewer
+        :param kwargs:
+        :return: The filename of the rendered file
+        """
+        pass
+
+
 class PlaybookBuilder(ABC):
+    """
+    This the base class to inherit from by the renderer to build a single Playbook in the target format.
+    It provides some methods that need to be implemented
+    """
+
     def __init__(
         self,
         playbook_node: PlaybookNode,
@@ -93,11 +128,11 @@ def build_node(self, node: Node, color: str, fontcolor: str, **kwargs):
             )
 
     @abstractmethod
-    def build_playbook(self, **kwargs):
+    def build_playbook(self, **kwargs) -> str:
         """
         Build the whole playbook
         :param kwargs:
-        :return:
+        :return: The rendered playbook as a string
         """
         pass
 
@@ -111,6 +146,53 @@ def build_play(self, play_node: PlayNode, **kwargs):
         """
         pass
 
+    def traverse_play(self, play_node: PlayNode, **kwargs):
+        """
+        Traverse a play to build the graph: pre_tasks, roles, tasks, post_tasks
+        :param play_node:
+        :param kwargs:
+        :return:
+        """
+        color, play_font_color = play_node.colors
+        # pre_tasks
+        for pre_task in play_node.pre_tasks:
+            self.build_node(
+                node=pre_task,
+                color=color,
+                fontcolor=play_font_color,
+                node_label_prefix="[pre_task] ",
+                **kwargs,
+            )
+
+        # roles
+        for role in play_node.roles:
+            self.build_role(
+                color=color,
+                fontcolor=play_font_color,
+                role_node=role,
+                **kwargs,
+            )
+
+        # tasks
+        for task in play_node.tasks:
+            self.build_node(
+                node=task,
+                color=color,
+                fontcolor=play_font_color,
+                node_label_prefix="[task] ",
+                **kwargs,
+            )
+
+        # post_tasks
+        for post_task in play_node.post_tasks:
+            self.build_node(
+                node=post_task,
+                color=color,
+                fontcolor=play_font_color,
+                node_label_prefix="[post_task] ",
+                **kwargs,
+            )
+
     @abstractmethod
     def build_task(self, task_node: TaskNode, color: str, fontcolor: str, **kwargs):
         """
diff --git a/ansibleplaybookgrapher/renderer/graphviz/__init__.py b/ansibleplaybookgrapher/renderer/graphviz/__init__.py
index e087118b..1d114732 100644
--- a/ansibleplaybookgrapher/renderer/graphviz/__init__.py
+++ b/ansibleplaybookgrapher/renderer/graphviz/__init__.py
@@ -18,14 +18,14 @@
 from ansible.utils.display import Display
 from graphviz import Digraph
 
-from ansibleplaybookgrapher.graph import (
+from ansibleplaybookgrapher.graph_model import (
     PlaybookNode,
     PlayNode,
     RoleNode,
     BlockNode,
     TaskNode,
 )
-from ansibleplaybookgrapher.renderer import PlaybookBuilder
+from ansibleplaybookgrapher.renderer import PlaybookBuilder, Renderer
 from ansibleplaybookgrapher.renderer.graphviz.postprocessor import GraphvizPostProcessor
 
 display = Display()
@@ -39,7 +39,7 @@
 }
 
 
-class GraphvizRenderer:
+class GraphvizRenderer(Renderer):
     def __init__(
         self,
         playbook_nodes: List[PlaybookNode],
@@ -52,7 +52,6 @@ def render(
         self,
         open_protocol_handler: str,
         open_protocol_custom_formats: Dict[str, str],
-        save_dot_file: bool,
         output_filename: str,
         view: bool,
         **kwargs,
@@ -60,6 +59,8 @@ def render(
         """
         :return: The filename where the playbooks where rendered
         """
+        save_dot_file = kwargs.get("save_dot_file", False)
+
         # Set of the roles that have been built so far for all the playbooks
         roles_built = set()
         digraph = Digraph(
@@ -219,6 +220,13 @@ def build_role(self, role_node: RoleNode, color: str, fontcolor: str, **kwargs):
         Render a role in the graph
         :return:
         """
+
+        # check if we already built this role
+        if role_node in self.roles_built:
+            return
+
+        self.roles_built.add(role_node)
+
         digraph = kwargs["digraph"]
 
         if role_node.include_role:  # For include_role, we point to a file
@@ -240,12 +248,6 @@ def build_role(self, role_node: RoleNode, color: str, fontcolor: str, **kwargs):
             labeltooltip=role_edge_label,
         )
 
-        # check if we already built this role
-        if role_node in self.roles_built:
-            return
-
-        self.roles_built.add(role_node)
-
         plays_using_this_role = self.roles_usage[role_node]
         if len(plays_using_this_role) > 1:
             # If the role is used in multiple plays, we take black as the default color
@@ -274,10 +276,10 @@ def build_role(self, role_node: RoleNode, color: str, fontcolor: str, **kwargs):
                     digraph=role_subgraph,
                 )
 
-    def build_playbook(self, **kwargs):
+    def build_playbook(self, **kwargs) -> str:
         """
         Convert the PlaybookNode to the graphviz dot format
-        :return:
+        :return: The text representation of the graphviz dot format for the playbook
         """
         display.vvv(f"Converting the graph to the dot format for graphviz")
         # root node
@@ -293,6 +295,8 @@ def build_playbook(self, **kwargs):
             with self.digraph.subgraph(name=play.name) as play_subgraph:
                 self.build_play(play, digraph=play_subgraph, **kwargs)
 
+        return self.digraph.source
+
     def build_play(self, play_node: PlayNode, **kwargs):
         """
 
@@ -320,7 +324,7 @@ def build_play(self, play_node: PlayNode, **kwargs):
             URL=self.get_node_url(play_node, "file"),
         )
 
-        # edge from root node to play
+        # from playbook to play
         playbook_to_play_label = f"{play_node.index} {play_node.name}"
         self.digraph.edge(
             self.playbook_node.id,
@@ -333,41 +337,5 @@ def build_play(self, play_node: PlayNode, **kwargs):
             labeltooltip=playbook_to_play_label,
         )
 
-        # pre_tasks
-        for pre_task in play_node.pre_tasks:
-            self.build_node(
-                node=pre_task,
-                color=color,
-                fontcolor=play_font_color,
-                node_label_prefix="[pre_task] ",
-                **kwargs,
-            )
-
-        # roles
-        for role in play_node.roles:
-            self.build_role(
-                color=color,
-                fontcolor=play_font_color,
-                role_node=role,
-                **kwargs,
-            )
-
-        # tasks
-        for task in play_node.tasks:
-            self.build_node(
-                node=task,
-                color=color,
-                fontcolor=play_font_color,
-                node_label_prefix="[task] ",
-                **kwargs,
-            )
-
-        # post_tasks
-        for post_task in play_node.post_tasks:
-            self.build_node(
-                node=post_task,
-                color=color,
-                fontcolor=play_font_color,
-                node_label_prefix="[post_task] ",
-                **kwargs,
-            )
+        # traverse the play
+        self.traverse_play(play_node, **kwargs)
diff --git a/ansibleplaybookgrapher/renderer/graphviz/postprocessor.py b/ansibleplaybookgrapher/renderer/graphviz/postprocessor.py
index c7061b7b..14eb02b5 100644
--- a/ansibleplaybookgrapher/renderer/graphviz/postprocessor.py
+++ b/ansibleplaybookgrapher/renderer/graphviz/postprocessor.py
@@ -19,7 +19,7 @@
 from lxml import etree
 from svg.path import parse_path
 
-from ansibleplaybookgrapher.graph import PlaybookNode
+from ansibleplaybookgrapher.graph_model import PlaybookNode
 
 display = Display()
 
diff --git a/ansibleplaybookgrapher/renderer/mermaid.py b/ansibleplaybookgrapher/renderer/mermaid.py
new file mode 100644
index 00000000..1458cf4c
--- /dev/null
+++ b/ansibleplaybookgrapher/renderer/mermaid.py
@@ -0,0 +1,337 @@
+# Copyright (C) 2023 Mohamed El Mouctar HAIDARA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from pathlib import Path
+from typing import Dict, Set, List
+
+from ansible.utils.display import Display
+
+from ansibleplaybookgrapher import BlockNode, RoleNode, TaskNode, PlayNode, PlaybookNode
+from ansibleplaybookgrapher.renderer import PlaybookBuilder, Renderer
+
+display = Display()
+
+# Default directive when rendering the graph.
+# More info at
+#   https://mermaid.js.org/config/directives.html
+#
+DEFAULT_DIRECTIVE = '%%{ init: { "flowchart": { "curve": "bumpX" } } }%%'
+DEFAULT_ORIENTATION = "LR"  # Left to right
+
+
+class MermaidFlowChartRenderer(Renderer):
+    def __init__(
+        self,
+        playbook_nodes: List[PlaybookNode],
+        roles_usage: Dict["RoleNode", Set[PlayNode]],
+    ):
+        self.playbook_nodes = playbook_nodes
+        self.roles_usage = roles_usage
+
+    def render(
+        self,
+        open_protocol_handler: str,
+        open_protocol_custom_formats: Dict[str, str],
+        output_filename: str,
+        view: bool,
+        **kwargs,
+    ) -> str:
+        """
+
+        :param open_protocol_handler:
+        :param open_protocol_custom_formats:
+        :param output_filename: without any extension
+        :param view:
+        :param kwargs:
+        :return:
+        """
+        # TODO: Add support for protocol handler
+        # TODO: Add support for hover
+        mermaid_code = "---\n"
+        mermaid_code += "title: Ansible Playbook Grapher\n"
+        mermaid_code += "---\n"
+
+        directive = kwargs.get("directive", DEFAULT_DIRECTIVE)
+        orientation = kwargs.get("orientation", DEFAULT_ORIENTATION)
+
+        display.vvv(f"Using '{directive}' as directive for the mermaid chart")
+        mermaid_code += f"{directive}\n"
+
+        mermaid_code += f"flowchart {orientation}\n"
+
+        # Mermaid only supports adding style to links by using the order of the link when it is created
+        # https://mermaid.js.org/syntax/flowchart.html#styling-links
+        link_order = 0
+
+        # Set of the roles that have been built so far for all the playbooks
+        roles_built = set()
+        for playbook_node in self.playbook_nodes:
+            playbook_builder = MermaidFlowChartPlaybookBuilder(
+                playbook_node=playbook_node,
+                open_protocol_handler=open_protocol_handler,
+                open_protocol_custom_formats=open_protocol_custom_formats,
+                roles_usage=self.roles_usage,
+                roles_built=roles_built,
+                link_order=link_order,
+            )
+
+            mermaid_code += playbook_builder.build_playbook()
+            link_order = playbook_builder.link_order
+            roles_built.update(playbook_builder.roles_built)
+
+        final_output_path_file = Path(f"{output_filename}.mmd")
+        # Make the sure the parents directories exist
+        final_output_path_file.parent.mkdir(exist_ok=True, parents=True)
+        final_output_path_file.write_text(mermaid_code)
+
+        display.display(
+            f"Mermaid code written to {final_output_path_file}", color="green"
+        )
+
+        if view:
+            # TODO: implement the view option
+            #  https://github.com/mermaidjs/mermaid-live-editor/issues/41 and https://mermaid.ink/
+            display.warning(
+                "The --view option is not supported yet by the mermaid renderer"
+            )
+
+        return final_output_path_file
+
+
+class MermaidFlowChartPlaybookBuilder(PlaybookBuilder):
+    def __init__(
+        self,
+        playbook_node: PlaybookNode,
+        open_protocol_handler: str,
+        open_protocol_custom_formats: Dict[str, str],
+        roles_usage: Dict[RoleNode, Set[PlayNode]],
+        roles_built: Set[RoleNode],
+        link_order: int = 0,
+    ):
+        super().__init__(
+            playbook_node,
+            open_protocol_handler,
+            open_protocol_custom_formats,
+            roles_usage,
+            roles_built,
+        )
+        self.mermaid_code = ""
+        # Used as an identifier for the links
+        self.link_order = link_order
+        # The current depth level of the nodes. Used for indentation
+        self._identation_level = 1
+
+    def build_playbook(self, **kwargs) -> str:
+        """
+        Build the playbook
+        :param kwargs:
+        :return:
+        """
+        display.vvv(
+            f"Converting the playbook '{self.playbook_node.name}' to mermaid format"
+        )
+
+        # Playbook node
+        self.add_comment(f"Start of the playbook '{self.playbook_node.name}'")
+        self.add_text(f'{self.playbook_node.id}("{self.playbook_node.name}")')
+
+        self._identation_level += 1
+        for play_node in self.playbook_node.plays:
+            self.build_play(play_node)
+        self._identation_level -= 1
+
+        self.add_comment(f"End of the playbook '{self.playbook_node.name}'\n")
+
+        return self.mermaid_code
+
+    def build_play(self, play_node: PlayNode, **kwargs):
+        """
+
+        :param play_node:
+        :param kwargs:
+        :return:
+        """
+        # Play node
+        color, play_font_color = play_node.colors
+        self.add_comment(f"Start of the play '{play_node.name}'")
+
+        self.add_text(f'{play_node.id}["{play_node.name}"]')
+        self.add_text(f"style {play_node.id} fill:{color},color:{play_font_color}")
+
+        # From playbook to play
+        self.add_link(
+            source_id=play_node.parent.id,
+            text=f"{play_node.index}",
+            dest_id=play_node.id,
+            style=f"stroke:{color},color:{color}",
+        )
+
+        # traverse the play
+        self._identation_level += 1
+        self.traverse_play(play_node)
+        self._identation_level -= 1
+
+        self.add_comment(f"End of the play '{play_node.name}'")
+
+    def build_task(self, task_node: TaskNode, color: str, fontcolor: str, **kwargs):
+        """
+
+        :param task_node:
+        :param color:
+        :param fontcolor:
+        :param kwargs:
+        :return:
+        """
+        node_label_prefix = kwargs.get("node_label_prefix", "")
+        # Task node
+        self.add_text(f'{task_node.id}["{node_label_prefix}{task_node.name}"]')
+        self.add_text(f"style {task_node.id} stroke:{color},fill:{fontcolor}")
+
+        # From parent to task
+        self.add_link(
+            source_id=task_node.parent.id,
+            text=f"{task_node.index} {task_node.when}",
+            dest_id=task_node.id,
+            style=f"stroke:{color},color:{color}",
+        )
+
+    def build_role(self, role_node: RoleNode, color: str, fontcolor: str, **kwargs):
+        """
+
+        :param role_node:
+        :param color:
+        :param fontcolor:
+        :param kwargs:
+        :return:
+        """
+
+        # check if we already built this role
+        if role_node in self.roles_built:
+            return
+        self.roles_built.add(role_node)
+
+        # Role node
+        self.add_comment(f"Start of the role '{role_node.name}'")
+        self.add_text(f'{role_node.id}("[role] {role_node.name}")')
+        self.add_text(
+            f"style {role_node.id} fill:{color},color:{fontcolor},stroke:{color}"
+        )
+
+        # from parent to role
+        self.add_link(
+            source_id=role_node.parent.id,
+            text=f"{role_node.index} {role_node.when}",
+            dest_id=role_node.id,
+            style=f"stroke:{color},color:{color}",
+        )
+
+        # role tasks
+        self._identation_level += 1
+        for role_task in role_node.tasks:
+            self.build_node(
+                node=role_task,
+                color=color,
+                fontcolor=fontcolor,
+            )
+        self._identation_level -= 1
+        self.add_comment(f"End of the role '{role_node.name}'")
+
+    def build_block(self, block_node: BlockNode, color: str, fontcolor: str, **kwargs):
+        """
+
+        :param block_node:
+        :param color:
+        :param fontcolor:
+        :param kwargs:
+        :return:
+        """
+
+        # Block node
+        self.add_comment(f"Start of the block '{block_node.name}'")
+        self.add_text(f'{block_node.id}["[block] {block_node.name}"]')
+        self.add_text(
+            f"style {block_node.id} fill:{color},color:{fontcolor},stroke:{color}"
+        )
+
+        # from parent to block
+        self.add_link(
+            source_id=block_node.parent.id,
+            text=f"{block_node.index} {block_node.when}",
+            dest_id=block_node.id,
+            style=f"stroke:{color},color:{color}",
+        )
+
+        self.add_text(f'subgraph subgraph_{block_node.id}["{block_node.name} "]')
+
+        self._identation_level += 1
+        for task in block_node.tasks:
+            self.build_node(
+                node=task,
+                color=color,
+                fontcolor=fontcolor,
+            )
+        self._identation_level -= 1
+
+        self.add_text("end")  # End of the subgraph
+        self.add_comment(f"End of the block '{block_node.name}'")
+
+    def add_link(
+        self,
+        source_id: str,
+        text: str,
+        dest_id: str,
+        style: str = "",
+        link_type: str = "--",
+    ):
+        """
+        Add link between two nodes
+        :param source_id: The link source
+        :param text: The text on the link
+        :param dest_id: The link destination
+        :param style: The style to apply to the link
+        :param link_type: Type of link to create. https://mermaid.js.org/syntax/flowchart.html#links-between-nodes
+        :return:
+        """
+        # Replace double quotes with single quotes. Mermaid doesn't like double quotes
+        text = text.replace('"', "'").strip()
+        self.add_text(f'{source_id} {link_type}> |"{text}"| {dest_id}')
+
+        if style != "" or style is not None:
+            self.add_text(f"linkStyle {self.link_order} {style}")
+
+        self.link_order += 1
+
+    def add_comment(self, text: str):
+        """
+        Add a comment to the mermaid code
+        :param text: The text used as a comment
+        :return:
+        """
+        self.mermaid_code += f"{self.indentation}%% {text}\n"
+
+    def add_text(self, text: str):
+        """
+        Add a text to the mermaid diagram
+        :param text:
+        :return:
+        """
+        self.mermaid_code += f"{self.indentation}{text}\n"
+
+    @property
+    def indentation(self) -> str:
+        """
+        Return the current indentation level as tabulations
+        :return:
+        """
+        return "\t" * self._identation_level
diff --git a/img/block.png b/img/block.png
index 88529384..96fa8f20 100644
Binary files a/img/block.png and b/img/block.png differ
diff --git a/img/example.png b/img/example.png
index ae7a9372..2674e317 100644
Binary files a/img/example.png and b/img/example.png differ
diff --git a/img/with_roles.png b/img/with_roles.png
index 10e743bf..a2d37c92 100644
Binary files a/img/with_roles.png and b/img/with_roles.png differ


Test patch:
diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml
index 5c4f0308..01a21c22 100644
--- a/.github/workflows/testing.yaml
+++ b/.github/workflows/testing.yaml
@@ -17,6 +17,7 @@ jobs:
     name: Tests Py${{ matrix.python-version }} - Ansible ${{ matrix.ansible-version }}
     env:
       SVG_FILES_PATH: tests/generated-svgs
+      MERMAID_FILES_PATH: tests/generated-mermaids
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
@@ -75,12 +76,8 @@ jobs:
 #          branch: generated-svgs-${{ github.head_ref || github.ref }}-${{ env.MATRIX_JOB_IDENTIFIER }}
 #          create_branch: true
 #
-#      - name: Publish job summary
-#        env:
-#          SVG_COMMIT_SHA: ${{ steps.commit-svg-files.outputs.commit_hash }}
-#        run: |
-#          env
-#          python tests/generate-job-summary.py >> $GITHUB_STEP_SUMMARY
+      - name: Publish job summary
+        run: python tests/generate-job-summary.py >> $GITHUB_STEP_SUMMARY
 
       - name: Test installation in virtualenv
         run: make test_install ANSIBLE_VERSION=${{ matrix.ansible-version }}
diff --git a/tests/generate-job-summary.py b/tests/generate-job-summary.py
index d5a4a2bc..522d083c 100644
--- a/tests/generate-job-summary.py
+++ b/tests/generate-job-summary.py
@@ -7,9 +7,9 @@
 DIR_PATH = os.path.dirname(os.path.realpath(__file__))
 
 
-def list_svg_files(path_pattern: str) -> List[str]:
+def list_files(path_pattern: str) -> List[str]:
     """
-
+    Return the list of files matching the pattern
     :param path_pattern:
     :return:
     """
@@ -20,11 +20,10 @@ def list_svg_files(path_pattern: str) -> List[str]:
     with open(os.path.join(DIR_PATH, "job-summary.md.j2")) as template_file:
         template = Template(template_file.read())
 
-    svg_files = list_svg_files(f"{os.environ['SVG_FILES_PATH']}/*.svg")
-    links = []
-    for f in svg_files:
-        links.append(
-            f"https://raw.githubusercontent.com/{os.environ['GITHUB_REPOSITORY']}/{os.environ['SVG_COMMIT_SHA']}/{f}"
-        )
+    mermaid_files = list_files(f"{os.environ['MERMAID_FILES_PATH']}/*.mmd")
+    matrix_job_identifier = os.environ["MATRIX_JOB_IDENTIFIER"]
+    files = []
+    for filename in mermaid_files:
+        files.append({"name": filename, "content": open(filename).read()})
 
-    print(template.render(svg_files=links))
+    print(template.render(files=files, matrix_job_identifier=matrix_job_identifier))
diff --git a/tests/job-summary.md.j2 b/tests/job-summary.md.j2
index 92cec068..5f33aaf8 100644
--- a/tests/job-summary.md.j2
+++ b/tests/job-summary.md.j2
@@ -1,6 +1,14 @@
-### Generated SVG files
-| Images  |
-|:-------:|
-{% for svg_file in svg_files -%}
-| ![svg]({{ svg_file }}) |
+## Generated Mermaid files
+
+This is the list of all the mermaid graph generated by the tests.
+
+**Note:** It doesn't match the generated SVGs graphs yet in the artifacts.
+
+{% for file in files -%}
+### {{ file.name }} - {{ matrix_job_identifier }}
+
+```mermaid
+{{ file.content }}
+```
+
 {% endfor %}
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 5810747f..93b47471 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -85,6 +85,20 @@ def test_cli_output_filename(output_filename_option, expected):
     assert cli.options.output_filename == expected
 
 
+def test_cli_output_filename_multiple_playbooks():
+    """
+    Test for the output filename when using multiple playbooks
+    :return:
+    """
+    args = [__prog__] + ["playbook.yml", "second-playbook.yml", "third-playbook.yaml"]
+
+    cli = PlaybookGrapherCLI(args)
+
+    cli.parse()
+
+    assert cli.options.output_filename == "playbook-second-playbook-third-playbook"
+
+
 @pytest.mark.parametrize(
     "include_role_tasks_option, expected",
     [(["--"], False), (["--include-role-tasks"], True)],
diff --git a/tests/test_graph.py b/tests/test_graph_model.py
similarity index 97%
rename from tests/test_graph.py
rename to tests/test_graph_model.py
index 003def0e..86bc9882 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph_model.py
@@ -1,4 +1,4 @@
-from ansibleplaybookgrapher.graph import (
+from ansibleplaybookgrapher.graph_model import (
     RoleNode,
     TaskNode,
     PlayNode,
diff --git a/tests/test_postprocessor.py b/tests/test_graphviz_postprocessor.py
similarity index 97%
rename from tests/test_postprocessor.py
rename to tests/test_graphviz_postprocessor.py
index 7a78223e..a51e4bce 100644
--- a/tests/test_postprocessor.py
+++ b/tests/test_graphviz_postprocessor.py
@@ -3,7 +3,7 @@
 import pytest
 from lxml import etree
 
-from ansibleplaybookgrapher.graph import PlaybookNode, PlayNode, TaskNode
+from ansibleplaybookgrapher.graph_model import PlaybookNode, PlayNode, TaskNode
 from ansibleplaybookgrapher.renderer.graphviz.postprocessor import (
     GraphvizPostProcessor,
     SVG_NAMESPACE,
diff --git a/tests/test_playbook_grapher.py b/tests/test_graphviz_renderer.py
similarity index 97%
rename from tests/test_playbook_grapher.py
rename to tests/test_graphviz_renderer.py
index 0ce9acf0..ebb63679 100644
--- a/tests/test_playbook_grapher.py
+++ b/tests/test_graphviz_renderer.py
@@ -16,7 +16,7 @@
 
 def run_grapher(
     playbook_files: List[str],
-    output_filename: str = None,
+    output_filename: str,
     additional_args: List[str] = None,
 ) -> Tuple[str, List[str]]:
     """
@@ -24,7 +24,7 @@ def run_grapher(
     :param output_filename:
     :param additional_args:
     :param playbook_files:
-    :return: SVG path and playbook absolute path
+    :return: SVG path and playbooks absolute paths
     """
     additional_args = additional_args or []
     # Explicitly add verbosity to the tests
@@ -56,10 +56,10 @@ def run_grapher(
     playbook_paths = [os.path.join(FIXTURES_DIR, p_file) for p_file in playbook_files]
     args = [__prog__]
 
-    if output_filename:  # the default filename is the playbook file name minus .yml
-        # put the generated svg in a dedicated folder
-        output_filename = output_filename.replace("[", "-").replace("]", "")
-        args.extend(["-o", os.path.join(DIR_PATH, "generated-svgs", output_filename)])
+    # Clean the name a little bit
+    output_filename = output_filename.replace("[", "-").replace("]", "")
+    # put the generated file in a dedicated folder
+    args.extend(["-o", os.path.join(DIR_PATH, "generated-svgs", output_filename)])
 
     args.extend(additional_args)
 
@@ -94,12 +94,12 @@ def _common_tests(
     :return: A dictionary with the different tasks, roles, pre_tasks as keys and a list of Elements (nodes) as values
     """
 
-    pq = PyQuery(filename=svg_path)
-    pq.remove_namespaces()
-
     # test if the file exist. It will exist only if we write in it.
     assert os.path.isfile(svg_path), "The svg file should exist"
 
+    pq = PyQuery(filename=svg_path)
+    pq.remove_namespaces()
+
     playbooks = pq("g[id^='playbook_']")
     plays = pq("g[id^='play_']")
     tasks = pq("g[id^='task_']")
diff --git a/tests/test_mermaid_renderer.py b/tests/test_mermaid_renderer.py
new file mode 100644
index 00000000..38a858e3
--- /dev/null
+++ b/tests/test_mermaid_renderer.py
@@ -0,0 +1,120 @@
+import os
+from typing import List, Tuple
+
+import pytest
+
+from ansibleplaybookgrapher import __prog__
+from ansibleplaybookgrapher.cli import PlaybookGrapherCLI
+from tests import FIXTURES_DIR
+
+# This file directory abspath
+DIR_PATH = os.path.dirname(os.path.realpath(__file__))
+
+
+def run_grapher(
+    playbook_files: List[str],
+    output_filename: str = None,
+    additional_args: List[str] = None,
+) -> Tuple[str, List[str]]:
+    """
+    Utility function to run the grapher
+    :param output_filename:
+    :param additional_args:
+    :param playbook_files:
+    :return: Mermaid file path and playbooks absolute paths
+    """
+    additional_args = additional_args or []
+    # Explicitly add verbosity to the tests
+    additional_args.insert(0, "-vvv")
+
+    playbook_paths = [os.path.join(FIXTURES_DIR, p_file) for p_file in playbook_files]
+    args = [__prog__]
+
+    # Clean the name a little bit
+    output_filename = (
+        output_filename.replace("[", "-").replace("]", "").replace(".yml", "")
+    )
+    # put the generated file in a dedicated folder
+    args.extend(["-o", os.path.join(DIR_PATH, "generated-mermaids", output_filename)])
+
+    args.extend(additional_args)
+
+    args.extend(["--renderer", "mermaid-flowchart"])
+
+    args.extend(playbook_paths)
+
+    cli = PlaybookGrapherCLI(args)
+
+    return cli.run(), playbook_paths
+
+
+def _common_tests(mermaid_path: str, playbook_paths: List[str], **kwargs):
+    """
+
+    :param mermaid_path:
+    :param playbook_paths:
+    :param kwargs:
+    :return:
+    """
+
+    # TODO: add proper tests on the mermaid code.
+    #  Need a parser to make sure the outputs contain all the playbooks, plays, tasks and roles
+    # test if the file exist. It will exist only if we write in it.
+    assert os.path.isfile(
+        mermaid_path
+    ), f"The mermaid file should exist at '{mermaid_path}'"
+
+
+@pytest.mark.parametrize(
+    "playbook_file",
+    [
+        # FIXME: Once we have proper tests, we need to split the parameters similar to what we do with graphviz
+        "docker-mysql-galaxy.yml",
+        "example.yml",
+        "group-roles-by-name.yml",
+        "import_playbook.yml",
+        "import_role.yml",
+        "import_tasks.yml",
+        "include_role.yml",
+        "include_tasks.yml",
+        "multi-plays.yml",
+        "nested_import_playbook.yml",
+        "nested_include_tasks.yml",
+        "relative_var_files.yml",
+        "roles_dependencies.yml",
+        "simple_playbook.yml",
+        "tags.yml",
+        "with_block.yml",
+        "with_roles.yml",
+    ],
+)
+def test_playbook(request, playbook_file):
+    """
+    Test the renderer with a single playbook
+    """
+    mermaid_path, playbook_paths = run_grapher(
+        [playbook_file],
+        output_filename=request.node.name,
+        additional_args=[
+            "-i",
+            os.path.join(FIXTURES_DIR, "inventory"),
+            "--include-role-tasks",
+        ],
+    )
+    _common_tests(mermaid_path, playbook_paths)
+
+
+def test_multiple_playbooks(request):
+    """
+    Test the renderer with multiple playbooks in a single graph
+    """
+    mermaid_path, playbook_paths = run_grapher(
+        ["multi-plays.yml", "relative_var_files.yml", "with_roles.yml"],
+        output_filename=request.node.name,
+        additional_args=[
+            "-i",
+            os.path.join(FIXTURES_DIR, "inventory"),
+            "--include-role-tasks",
+        ],
+    )
+    _common_tests(mermaid_path, playbook_paths)
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 5de2ec92..87134ad1 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -6,7 +6,7 @@
 
 from ansibleplaybookgrapher import PlaybookParser
 from ansibleplaybookgrapher.cli import PlaybookGrapherCLI
-from ansibleplaybookgrapher.graph import (
+from ansibleplaybookgrapher.graph_model import (
     TaskNode,
     BlockNode,
     RoleNode,