├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── examples │ └── hello_world.toml ├── images │ ├── ocean_theme_example.png │ └── orientation_example.png └── skillmap_descriptor.md ├── justfile ├── poetry.lock ├── pyproject.toml ├── skillmap ├── __init__.py ├── main.py ├── nodes │ ├── __init__.py │ ├── common.py │ ├── group_node.py │ ├── progress_bar.py │ ├── skill_node.py │ └── skillmap_node.py ├── skillmap_parser.py ├── theme_loader.py └── themes │ ├── earth.theme │ ├── grape.theme │ ├── grass.theme │ ├── ocean.theme │ ├── pale.theme │ └── rose.theme └── tests ├── __init__.py ├── nodes ├── __init__.py ├── skill_node_test.py └── skillmap_node_test.py ├── skillmap_parser_test.py └── url_shortener.toml /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | tests/__pycache__ 3 | skillmap/__pycache__ 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.0 - 2022-03-02 2 | * support different styles of progress bar for skill 3 | # 0.2.3 - 2022-02-27 4 | * add a default skill map name and mark python 3.8 to be the min version 5 | # 0.2.2 - 2022-02-27 6 | * support using different orientations 7 | # 0.2.1 - 2022-02-27 8 | * disable a group if all its skills are new or not specified 9 | # 0.2.0 - 2022-02-27 10 | * add more themes 11 | # 0.1.0 - 2022-02-26 12 | * initial version, generating skill map from toml file to mermaid -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2022 Scott Chacon and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # skillmap 2 | A tool for generating skill map/tree like diagram. 3 | 4 | # What is a skill map/tree? 5 | Skill tree is a term used in video games, and it can be used for describing roadmaps for software project development as well. When you are building a software project, you can use the concept of skill tree/technology tree to describe the steps you need to take to build the project. 6 | 7 | > In strategy games, a technology, tech, or research tree is a hierarchical visual representation of the possible sequences of upgrades a player can take (most often through the act of research). Because these trees are technically directed and acyclic, they can more accurately be described as a technology directed acyclic graph. 8 | -- https://en.wikipedia.org/wiki/Technology_tree 9 | 10 | This project borrows inspiration and ideas from several sources: 11 | 1. https://hacks.mozilla.org/2018/10/webassemblys-post-mvp-future/ 12 | 2. https://github.com/nikomatsakis/skill-tree 13 | 3. https://en.wikipedia.org/wiki/Technology_tree 14 | 15 | # Features 16 | * skill tree/map generation 17 | * specify pre-requisite skills 18 | * multiple themes 19 | * multiple skill progress bar styles 20 | # Installation 21 | ``` 22 | pip install skillmap 23 | ``` 24 | After installation, a `skillmap` command is available. 25 | 26 | # Usage 27 | 1. Create a toml format skill map descriptor file. You can find more details about this descriptor format [here](docs/skillmap_descriptor.md). For a minimal example, see [`docs/examples/hello_world.toml`](docs/examples/hello_world.toml) 28 | ``` 29 | [skillmap] 30 | name = "hello world" 31 | icon = "bicycle" 32 | 33 | [groups.learn_python] 34 | name = "learn python" 35 | icon = "rocket" 36 | [groups.learn_python.skills.print] 37 | name = "print statement" 38 | icon = "printer" 39 | [groups.learn_python.skills.string] 40 | name = "string literal" 41 | icon = "book" 42 | ``` 43 | 44 | 2. Run `skillmap path/to/your/skillmap.toml` 45 | 1. For example, `skillmap docs/examples/hello_world.toml` 46 | 3. Copy the generated skill map diagram to your clipboard. 47 | 4. Paste the diagram to a mermaid diagram editor, for example, [`https://mermaid-js.github.io/mermaid-live-editor`](https://mermaid-js.github.io/mermaid-live-editor). 48 | 49 | # Examples 50 | ![ocean_theme_example](docs/images/ocean_theme_example.png) 51 | ![orientation_example](docs/images/orientation_example.png) 52 | 53 | * Each node can have a string label and an fontawsome icon. 54 | * Skills with different statuses will be shown with different colors. 55 | * Each skill may have a progress bar to indicate its learning progress. 56 | * Pre-requisite skills will be connected with an directed edge. 57 | * You can embed the generated mermaid diagram into github markdown directly, but the fontawesome icons in the diagrams are not shown by github so far. 58 | * Unnamed skill (a skill node only has its toml table id but without any table property) will be shown as a locked skill. I find this is a useful analogy when you have a problem in your project that you don't have any clue how it should be solved yet. 59 | * You can find the skill map toml for the above exmaples [here](tests/url_shortener.toml) 60 | 61 | # License 62 | [MIT License](LICENSE) 63 | 64 | # More details 65 | * Skillmap toml descriptor format can be found [here](docs/skillmap_descriptor.md) 66 | * hot reloading when authoring a skillmap toml file 67 | * install several tools to make hot reloading to work 68 | * [`entr`](https://github.com/eradman/entr), run arbitrary commands when files change 69 | * [Visual Studio Code](https://code.visualstudio.com) + [Markdown Preview Enhanced Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced) 70 | * Basically, use `entr` to watch toml file changes, and generate a `md` makrdown file using `skillmap` every time when toml file changes. And use `vscode` + `Markdown Preview Enhanced` extension to open this generated markdown file. Check out `build_sample` and `dev_sample` in [justfile](justfile) to see how to make hot reloading work 71 | # Known issues 72 | * Sometimes, the group's text will be clipped when rendered in mermaid. And you have to edit the generated file slightly and then change it back to ask mermaid to refersh the diagram to avoid clipping. It is probably a bug for mermaid as far as I can tell. -------------------------------------------------------------------------------- /docs/examples/hello_world.toml: -------------------------------------------------------------------------------- 1 | [skillmap] 2 | name = "hello world" 3 | icon = "bicycle" 4 | 5 | [groups.learn_python] 6 | name = "learn python" 7 | icon = "rocket" 8 | [groups.learn_python.skills.print] 9 | name = "print statement" 10 | icon = "printer" 11 | [groups.learn_python.skills.string] 12 | name = "string literal" 13 | icon = "book" 14 | -------------------------------------------------------------------------------- /docs/images/ocean_theme_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niyue/skillmap/ca1011d5f822134ad1d7c5f7f243da30a0731170/docs/images/ocean_theme_example.png -------------------------------------------------------------------------------- /docs/images/orientation_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niyue/skillmap/ca1011d5f822134ad1d7c5f7f243da30a0731170/docs/images/orientation_example.png -------------------------------------------------------------------------------- /docs/skillmap_descriptor.md: -------------------------------------------------------------------------------- 1 | # Skillmap descriptor format 2 | * The skillmap descriptor format is a toml file. Here is a minimal example, see [`examples/hello_world.toml`](examples/hello_world.toml) 3 | * It describes three concepts in the file: 4 | * `skillmap`: the root key of the toml file. Typically, it is used to represent a project. 5 | * `group`: a group of skills. In each file, it can have multiple groups in it. You can use `group` to represent sub project/component in a project. 6 | * `skill`: a specific skill. Each group can have multiple skills in it. You can use `skill` to represent a specific component/module in a sub project/component. 7 | ### skillmap toml table 8 | * The `skillmap` toml table can have some fields: 9 | * `name`: [optional] the name of the skillmap. It will be used as a label in the diagram for the top level node. 10 | * `icon`: [optional] a fontawsome icon name. It will be used as an icon in the diagram. You can find the fontawsome icon list [here](https://fontawesome.com/v4.7.0/icons/). 11 | * `theme`: [optional] theme for the diagram. Serveral themes are included: 12 | * ocean (default theme) 13 | * earth 14 | * grape 15 | * grass 16 | * pale 17 | * rose 18 | * `orientation`: [optional] the orientation of the diagram. [All mermaid's orientations](https://mermaid-js.github.io/mermaid/#/flowchart?id=flowchart-orientation) are supported, including: 19 | * TB - top to bottom (default) 20 | * TD - top-down/ same as top to bottom 21 | * BT - bottom to top 22 | * RL - right to left 23 | * LR - left to right 24 | * `progress_bar_style`: [optional] an integer value as different styles of progress bar. When a skill is specified with a progress (e.g. "1/3"), this property can be used to tell skillmap to render the skill progress bar in different styles. There are currently 28 different styles you can choose from. Simply specify a value between `0` ~ `27` to give it a try. For example, here are two styles `💚🤍🤍`/`■□□` you can choose from. 25 | ## group/skill toml tables 26 | * The `group`/`skill` toml table can have some fields: 27 | * `name`: [optional] the name of the skillmap/group/skill. It will be used as a label in the diagram. . 28 | * `icon`: [optional] a fontawsome icon name. It will be used as an icon in the diagram. You can find the fontawsome icon list [here](https://fontawesome.com/v4.7.0/icons/). 29 | * `requires`: [optional] a list of strings. It indicates a list of skill groups or skills to be learned before this learning this skill group/skill. where each string is a toml table name of a group/skill. It will be rendered as an edge(s) from one node to another. 30 | * `progress`: [optional] only applies to a `skill` toml table. It is a fraction number string like `1/3` that indicates the learning progression of the skill. A progress bar like `■□□` will be shown in the skill node to visualize the progress. Skill nodes will be rendered with different colors according to different progresses (zero progress, ongoing, finished). 31 | * `status`: [optional] a string value indicating the status of the skill. Three valid values are supported [new|beingLearned|learned]. When specifying a different status, different colors will be used when rendering the node. Usually, you simply use `progress` to indicate the status. If you do NOT want a progress bar but just want some simple status, you can specify this property. 32 | * locked skill: if a skill table doesn't have name or icon, it will be rendered as a locked skill (a grey box + lock icon + `???` as name). 33 | 34 | ## Example 35 | ```toml 36 | [groups.learn_python] 37 | name = "learn python" 38 | icon = "rocket" 39 | [groups.learn_python.skills.string_literal_usage] 40 | name = "string literal" 41 | icon = "book" 42 | progress = "1/3" 43 | [groups.learn_python.skills.print] 44 | name = "print statement" 45 | icon = "printer" 46 | requires = ["groups.learn_python.skills.string_literal_usage"] 47 | 48 | [groups.program_with_python] 49 | name = "program with python" 50 | icon = "car" 51 | requires = ["groups.learn_python"] 52 | ``` 53 | In this exmaple, there are: 54 | * two groups: `groups.learn_python` and `groups.program_with_python` 55 | * `groups.learn_python` has two skills: 56 | * `groups.learn_python.skills.string_literal_usage` 57 | * this skill's learning progress is `1/3` 58 | * `groups.learn_python.skills.print` 59 | * this skill requires `groups.learn_python.skills.string_literal_usage` to be learned first 60 | * `groups.program_with_python` requires `groups.learn_python` to be learned first. When drawn in the diagram, it will be rendered as an edge from `groups.learn_python` to `groups.program_with_python`. -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env just --justfile 2 | set dotenv-load := true 3 | 4 | sample_toml := "tests/url_shortener.toml" 5 | sample_md := "dist/url_shortener.md" 6 | sample_png := "dist/url_shortener.png" 7 | built_wheel := "./dist/skillmap-*-py3-none-any.whl" 8 | 9 | # build and install package into system python for every python file change 10 | dev: 11 | find skillmap -iname "*.py" -iname "pyproject.toml" | entr -s "poetry build && pip install {{ built_wheel }} --force-reinstall" 12 | 13 | # install package into system python 14 | setup: 15 | # install the library into system python 16 | rm -fr ./dist 17 | poetry build && pip install {{ built_wheel }} --force-reinstall 18 | 19 | # publish package to pypi 20 | publish: 21 | poetry build 22 | poetry publish 23 | 24 | # generate markdown from source toml file 25 | generate src dest: 26 | echo '```mermaid' > {{ dest }} && poetry run skillmap {{ src }} >> {{ dest }} && echo '```' >> {{ dest }} 27 | 28 | # generate png from source toml file 29 | png src dest: 30 | # mermaid cli (https://github.com/mermaid-js/mermaid-cli) needs to be installed 31 | poetry run skillmap {{ src }} | mmdc -o {{ dest }} 32 | 33 | # generate markdown for sample skillmap 34 | generate_sample: 35 | just generate {{ sample_toml }} {{ sample_md }} 36 | 37 | # generate png for sample skillmap 38 | png_sample: 39 | just png {{ sample_toml }} {{ sample_png }} 40 | 41 | # develop sample skillmap by hot reloading the file and generated results 42 | dev_sample: 43 | find "tests" -iname "*.toml" | entr -s "just generate_sample" 44 | 45 | 46 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.0" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "21.4.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 19 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 22 | 23 | [[package]] 24 | name = "colorama" 25 | version = "0.4.4" 26 | description = "Cross-platform colored terminal text." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 30 | 31 | [[package]] 32 | name = "iniconfig" 33 | version = "1.1.1" 34 | description = "iniconfig: brain-dead simple config-ini parsing" 35 | category = "dev" 36 | optional = false 37 | python-versions = "*" 38 | 39 | [[package]] 40 | name = "packaging" 41 | version = "21.3" 42 | description = "Core utilities for Python packages" 43 | category = "dev" 44 | optional = false 45 | python-versions = ">=3.6" 46 | 47 | [package.dependencies] 48 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 49 | 50 | [[package]] 51 | name = "pluggy" 52 | version = "1.0.0" 53 | description = "plugin and hook calling mechanisms for python" 54 | category = "dev" 55 | optional = false 56 | python-versions = ">=3.6" 57 | 58 | [package.extras] 59 | dev = ["pre-commit", "tox"] 60 | testing = ["pytest", "pytest-benchmark"] 61 | 62 | [[package]] 63 | name = "py" 64 | version = "1.11.0" 65 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 66 | category = "dev" 67 | optional = false 68 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 69 | 70 | [[package]] 71 | name = "pyparsing" 72 | version = "3.0.7" 73 | description = "Python parsing module" 74 | category = "dev" 75 | optional = false 76 | python-versions = ">=3.6" 77 | 78 | [package.extras] 79 | diagrams = ["jinja2", "railroad-diagrams"] 80 | 81 | [[package]] 82 | name = "pytest" 83 | version = "6.2.5" 84 | description = "pytest: simple powerful testing with Python" 85 | category = "dev" 86 | optional = false 87 | python-versions = ">=3.6" 88 | 89 | [package.dependencies] 90 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 91 | attrs = ">=19.2.0" 92 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 93 | iniconfig = "*" 94 | packaging = "*" 95 | pluggy = ">=0.12,<2.0" 96 | py = ">=1.8.2" 97 | toml = "*" 98 | 99 | [package.extras] 100 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 101 | 102 | [[package]] 103 | name = "toml" 104 | version = "0.10.2" 105 | description = "Python Library for Tom's Obvious, Minimal Language" 106 | category = "main" 107 | optional = false 108 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 109 | 110 | [metadata] 111 | lock-version = "1.1" 112 | python-versions = "^3.8" 113 | content-hash = "20163799a61060bf824a23e75fffd14c653982838cfc4ad0e74150a0ea1db061" 114 | 115 | [metadata.files] 116 | atomicwrites = [ 117 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 118 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 119 | ] 120 | attrs = [ 121 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 122 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 123 | ] 124 | colorama = [ 125 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 126 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 127 | ] 128 | iniconfig = [ 129 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 130 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 131 | ] 132 | packaging = [ 133 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 134 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 135 | ] 136 | pluggy = [ 137 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 138 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 139 | ] 140 | py = [ 141 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 142 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 143 | ] 144 | pyparsing = [ 145 | {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, 146 | {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, 147 | ] 148 | pytest = [ 149 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 150 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 151 | ] 152 | toml = [ 153 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 154 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 155 | ] 156 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "skillmap" 3 | version = "0.3.0" 4 | description = "Skillmap generates a skill tree from a toml file" 5 | authors = ["Yue Ni "] 6 | readme = "README.md" 7 | homepage = "https://github.com/niyue/skillmap" 8 | repository = "https://github.com/niyue/skillmap" 9 | license = "MIT" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.8" 13 | toml = "^0.10.2" 14 | 15 | [tool.poetry.dev-dependencies] 16 | pytest = "^6.0" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=1.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | 22 | [tool.poetry.scripts] 23 | skillmap = 'skillmap.main:main' -------------------------------------------------------------------------------- /skillmap/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | 3 | -------------------------------------------------------------------------------- /skillmap/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import os 4 | from pathlib import Path 5 | 6 | import argparse 7 | from distutils.util import strtobool 8 | 9 | from skillmap.skillmap_parser import SkillMapParser 10 | from skillmap.nodes.skillmap_node import create_skillmap_graph 11 | 12 | import importlib.metadata 13 | 14 | package_metadada = importlib.metadata.metadata("skillmap") 15 | # info from pyproject.toml's `version` and `description` 16 | SKILLMAP_VERSION = package_metadada.get("Version") 17 | SKILLMAP_SUMMARY = package_metadada.get("Summary") 18 | 19 | 20 | def _skillmap_parser(): 21 | parser = argparse.ArgumentParser(prog="skillmap") 22 | parser.add_argument( 23 | "descriptor_toml", 24 | default=False, 25 | type=str, 26 | help="The path to a toml file describing the skillmap. You can find more deetails https://github.com/niyue/skillmap/blob/main/docs/skillmap_descriptor.md", 27 | ) 28 | parser.add_argument( 29 | "--version", 30 | action="version", 31 | version=f"%(prog)s {SKILLMAP_VERSION} [{SKILLMAP_SUMMARY}]", 32 | help="show version number", 33 | ) 34 | 35 | return parser 36 | 37 | 38 | def parse_sys_args(sys_args): 39 | parser = _skillmap_parser() 40 | args = parser.parse_args(sys_args) 41 | return vars(args) 42 | 43 | 44 | def generate(skillmap_file, format = None): 45 | skillmap_dict = SkillMapParser().parse(skillmap_file) 46 | skillmap_graph = create_skillmap_graph(skillmap_dict) 47 | return skillmap_graph 48 | 49 | 50 | def main(): 51 | args = parse_sys_args(sys.argv[1:]) 52 | skillmap_file = Path(args["descriptor_toml"]) 53 | # format = args["format"] 54 | skillmap_graph = generate(skillmap_file, format) 55 | print(skillmap_graph) 56 | -------------------------------------------------------------------------------- /skillmap/nodes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niyue/skillmap/ca1011d5f822134ad1d7c5f7f243da30a0731170/skillmap/nodes/__init__.py -------------------------------------------------------------------------------- /skillmap/nodes/common.py: -------------------------------------------------------------------------------- 1 | SECTION_SEPARATOR = "%" * 30 2 | 3 | def get_required_node_edges(qualified_node_id, required_node_ids): 4 | node_requires = [ 5 | f"{required_node_id}-->{qualified_node_id}" 6 | for required_node_id in required_node_ids 7 | ] 8 | return "\n".join(node_requires) 9 | 10 | 11 | def get_icon(dict_value): 12 | icon = "" 13 | if "icon" in dict_value: 14 | icon = f"fa:fa-{dict_value['icon']}" 15 | return icon 16 | 17 | 18 | def get_node_content(items, multi_lines_layout=True): 19 | separator = "
" if multi_lines_layout else " " 20 | # join non empty items 21 | return separator.join([i for i in items if i]) 22 | -------------------------------------------------------------------------------- /skillmap/nodes/group_node.py: -------------------------------------------------------------------------------- 1 | from skillmap.nodes.common import ( 2 | get_icon, 3 | get_node_content, 4 | get_required_node_edges, 5 | SECTION_SEPARATOR, 6 | ) 7 | from skillmap.nodes.skill_node import create_skill_node 8 | 9 | 10 | def _qualify(group_id): 11 | return f"groups.{group_id}" 12 | 13 | 14 | def _qualified_skill_id(qualified_group_id, skill_id): 15 | return f"{qualified_group_id}.skills.{skill_id}" 16 | 17 | 18 | def create_groups_edges(map_id, groups): 19 | group_ids = [ 20 | _qualify(group_id) 21 | for group_id, group_value in groups.items() 22 | if "requires" not in group_value # all groups without requires 23 | ] 24 | groups_edges = "\n".join([f"{map_id}-->{gid}" for gid in group_ids]) 25 | return groups_edges 26 | 27 | 28 | def get_group_skills_list(qualified_group_id, group_skills, progress_bar_style=0): 29 | group_skills = [ 30 | create_skill_node( 31 | _qualified_skill_id(qualified_group_id, skill_id), 32 | skill_value, 33 | progress_bar_style, 34 | ) 35 | for skill_id, skill_value in group_skills.items() 36 | ] 37 | return "\n".join(group_skills) 38 | 39 | 40 | def get_group_status(group_skills): 41 | for _, skill_value in group_skills.items(): 42 | skill_status = skill_value.get("status", "") 43 | if skill_status not in ("new", ""): 44 | return "normal" 45 | return "new" 46 | 47 | 48 | def create_group_subgraph(group_id, group_value, progress_bar_style=0): 49 | qualified_group_id = _qualify(group_id) 50 | group_name = group_value.get("name", "") 51 | group_icon = get_icon(group_value) 52 | group_icon_label = get_node_content([group_icon, group_name], False) 53 | group_skills_list = get_group_skills_list( 54 | qualified_group_id, group_value.get("skills", {}), progress_bar_style 55 | ) 56 | group_status = get_group_status(group_value.get("skills", {})) 57 | 58 | group_requires_list = get_required_node_edges( 59 | qualified_group_id, group_value.get("requires", []) 60 | ) 61 | 62 | group_id_and_name = f"subgraph {qualified_group_id}[\"{group_icon_label}\"]" 63 | group_style = f"class {qualified_group_id} {group_status}SkillGroup;" 64 | group_subgraph_end = "end" 65 | sections = [ 66 | SECTION_SEPARATOR, 67 | group_id_and_name, 68 | group_skills_list, 69 | group_subgraph_end, 70 | group_style, 71 | group_requires_list, 72 | ] 73 | 74 | group_graph = "\n".join(sections) 75 | return group_graph 76 | 77 | 78 | def create_group_subgraphs(groups, progress_bar_style=0): 79 | group_graphs = [ 80 | create_group_subgraph(group_id, group_value, progress_bar_style) 81 | for group_id, group_value in groups.items() 82 | ] 83 | return "\n\n".join(group_graphs) 84 | -------------------------------------------------------------------------------- /skillmap/nodes/progress_bar.py: -------------------------------------------------------------------------------- 1 | PROGRESS_BAR_STYLES = [ 2 | '□■', 3 | '▁█', 4 | '⣀⣿', 5 | '░█', 6 | '▒█', 7 | '□▩', 8 | '□▦', 9 | '▱▰', 10 | '▭◼', 11 | '▯▮', 12 | '◯⬤', 13 | '⚐⚑', 14 | '⬜⬛', 15 | '⬜🟩', 16 | '⬜🟦', 17 | '⬜🟧', 18 | '🤍💚', 19 | '🤍💙', 20 | '🤍🧡', 21 | '⚪⚫', 22 | '⚪🟢', 23 | '⚪🔵', 24 | '⚪🟠', 25 | '🌑🌕', 26 | '❕❗', 27 | '🥚🐣', 28 | '💣💥', 29 | '❌✅', 30 | ] -------------------------------------------------------------------------------- /skillmap/nodes/skill_node.py: -------------------------------------------------------------------------------- 1 | from skillmap.nodes.common import get_icon, get_node_content, get_required_node_edges 2 | from fractions import Fraction 3 | from enum import Enum 4 | from skillmap.nodes.progress_bar import PROGRESS_BAR_STYLES 5 | 6 | 7 | class SkillStatus(Enum): 8 | NEW = "new" 9 | BEING_LEANRED = "beingLearned" 10 | LEARNED = "learned" 11 | UNKNOWN = "unknown" 12 | 13 | 14 | def _is_locked_skill_value(skill_value): 15 | icon = skill_value.get("icon", None) 16 | status = skill_value.get("status", None) 17 | return icon == "lock" and status == "unknown" 18 | 19 | 20 | def get_progress(skill_value, progress_bar_style=0): 21 | if _is_locked_skill_value(skill_value): 22 | return ("", SkillStatus.UNKNOWN) 23 | 24 | progress_string = skill_value.get("progress", None) 25 | if progress_string: 26 | Fraction(progress_string) 27 | current, total = map(int, progress_string.split("/")) 28 | status = SkillStatus.NEW 29 | if current > 0: 30 | status = SkillStatus.BEING_LEANRED if current < total else SkillStatus.LEARNED 31 | 32 | chosen_progress_bar_style = PROGRESS_BAR_STYLES[ 33 | progress_bar_style % len(PROGRESS_BAR_STYLES) 34 | ] 35 | empty_cell, finished_cell = chosen_progress_bar_style 36 | return (f"{finished_cell * current}{empty_cell * (total - current)}", status) 37 | else: 38 | status_value = skill_value.get("status", "new") 39 | 40 | for s in [SkillStatus.NEW, SkillStatus.BEING_LEANRED, SkillStatus.LEARNED]: 41 | if status_value == s.value: 42 | return ("", s) 43 | 44 | 45 | def create_skill_node(skill_id, skill_value, progress_bar_style=0): 46 | if not skill_value: 47 | locked_skill_value = {"name": "???", "icon": "lock", "status": "unknown"} 48 | skill_value = locked_skill_value 49 | skill_name = skill_value.get("name", "") 50 | skill_icon = get_icon(skill_value) 51 | skill_progress, skill_status = get_progress(skill_value, progress_bar_style) 52 | skill_icon_label = get_node_content([skill_icon, skill_name, skill_progress]) 53 | skill_id_and_name = f'{skill_id}("{skill_icon_label}")' 54 | skill_style = f"class {skill_id} {skill_status.value}Skill;" 55 | skill_requires = get_required_node_edges(skill_id, skill_value.get("requires", [])) 56 | sections = [ 57 | skill_id_and_name, 58 | skill_style, 59 | skill_requires, 60 | ] 61 | skill_graph = "\n".join(sections) 62 | return skill_graph 63 | -------------------------------------------------------------------------------- /skillmap/nodes/skillmap_node.py: -------------------------------------------------------------------------------- 1 | from skillmap.theme_loader import load_theme 2 | from skillmap.nodes.common import get_icon, get_node_content, SECTION_SEPARATOR 3 | from skillmap.nodes.group_node import create_group_subgraphs, create_groups_edges 4 | import re 5 | 6 | 7 | def alphanumerize(s): 8 | return re.sub(r"\W+", "_", s) 9 | 10 | 11 | def get_orientation(skill_map_dict): 12 | orientation = skill_map_dict.get("orientation", "TD") 13 | if orientation not in ["TD", "TB", "BT", "RL", "LR"]: 14 | orientation = "TD" 15 | return orientation 16 | 17 | 18 | def get_progress_bar_style(skill_map_dict): 19 | progress_bar_style = 0 20 | try: 21 | progress_bar_style = int(skill_map_dict.get("progress_bar_style", 0)) 22 | except: 23 | pass 24 | return progress_bar_style 25 | 26 | 27 | # generate a mermaid graph from a skill map toml dict 28 | def create_skillmap_graph(skill_map): 29 | skill_map_dict = skill_map.get("skillmap", {}) 30 | map_name = skill_map_dict.get("name", "unamed_skill_map") 31 | map_id = alphanumerize(map_name) 32 | theme = skill_map_dict.get("theme", "ocean") 33 | progress_bar_style = get_progress_bar_style(skill_map_dict) 34 | orientation = get_orientation(skill_map_dict) 35 | map_icon = get_icon(skill_map_dict) 36 | map_icon_label = get_node_content([map_icon, map_name]) 37 | 38 | map_to_group_edges = create_groups_edges(map_id, skill_map.get("groups", {})) 39 | map_group_subgraphs = create_group_subgraphs( 40 | skill_map.get("groups", {}), progress_bar_style 41 | ) 42 | 43 | skill_map_node = f"{map_id}(\"{map_icon_label}\")" 44 | skill_map_node_style = f"class {map_id} normalSkillGroup;" 45 | skill_map_header = f"flowchart {orientation}" 46 | sections = [ 47 | skill_map_header, 48 | skill_map_node, 49 | map_group_subgraphs, 50 | SECTION_SEPARATOR, 51 | map_to_group_edges, 52 | load_theme(theme), 53 | skill_map_node_style, 54 | ] 55 | return "\n".join(sections) 56 | -------------------------------------------------------------------------------- /skillmap/skillmap_parser.py: -------------------------------------------------------------------------------- 1 | import toml 2 | 3 | class SkillMapParser: 4 | def __init__(self) -> None: 5 | pass 6 | 7 | def parse(self, skill_map_file: str) -> dict: 8 | return toml.load(skill_map_file) -------------------------------------------------------------------------------- /skillmap/theme_loader.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | import os 3 | 4 | def load_theme(theme): 5 | for try_theme in [theme, "ocean"]: 6 | theme_file = f"themes/{try_theme}.theme" 7 | try: 8 | theme_data = pkgutil.get_data(__name__, theme_file) 9 | if theme_data: 10 | return theme_data.decode("utf-8") 11 | except FileNotFoundError: 12 | pass -------------------------------------------------------------------------------- /skillmap/themes/earth.theme: -------------------------------------------------------------------------------- 1 | classDef normalSkillGroup stroke-width:4px,stroke:#F3D5B5,fill:#FFEDD8; 2 | classDef beingLearnedSkill stroke-width:2px,stroke:#D4A276,fill:#E7BC91; 3 | classDef learnedSkill stroke-width:2px,stroke:#A47148,fill:#BC8A5F; 4 | 5 | classDef newSkillGroup stroke-width:4px,stroke:#D6CCC2,fill:#EDEDE9; 6 | classDef newSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 7 | classDef unknownSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 8 | 9 | %% https://www.w3schools.com/colors/colors_groups.asp 10 | linkStyle default stroke-width:2px,stroke:BurlyWood; -------------------------------------------------------------------------------- /skillmap/themes/grape.theme: -------------------------------------------------------------------------------- 1 | classDef normalSkillGroup stroke-width:4px,stroke:#DAC3E8,fill:#DEC9E9; 2 | classDef beingLearnedSkill stroke-width:2px,stroke:#C19EE0,fill:#D2B7E5; 3 | classDef learnedSkill stroke-width:2px,stroke:#B185DB,fill:#C19EE0; 4 | 5 | classDef newSkillGroup stroke-width:4px,stroke:#D6CCC2,fill:#EDEDE9; 6 | classDef newSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 7 | classDef unknownSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 8 | 9 | %% https://www.w3schools.com/colors/colors_groups.asp 10 | linkStyle default stroke-width:2px,stroke:RebeccaPurple; -------------------------------------------------------------------------------- /skillmap/themes/grass.theme: -------------------------------------------------------------------------------- 1 | classDef normalSkillGroup stroke-width:4px,stroke:#99D98C,fill:#D9ED92; 2 | 3 | classDef beingLearnedSkill stroke-width:2px,stroke:#76C893,fill:#99D98C; 4 | classDef learnedSkill stroke-width:2px,stroke:#52B69A,fill:#76C893; 5 | 6 | classDef newSkillGroup stroke-width:4px,stroke:#D6CCC2,fill:#EDEDE9; 7 | classDef newSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 8 | classDef unknownSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 9 | 10 | %% https://www.w3schools.com/colors/colors_groups.asp 11 | linkStyle default stroke-width:2px,stroke:OliveDrab; -------------------------------------------------------------------------------- /skillmap/themes/ocean.theme: -------------------------------------------------------------------------------- 1 | classDef normalSkillGroup stroke:#0096C7,stroke-width:4px,fill:#CAF0F8; 2 | 3 | classDef beingLearnedSkill stroke-width:2px,stroke:#90E0EF,fill:#ADE8F4; 4 | classDef learnedSkill stroke-width:2px,stroke:#00B4D8,fill:#48CAE4; 5 | 6 | classDef newSkillGroup stroke-width:4px,stroke:#D6CCC2,fill:#EDEDE9; 7 | classDef newSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 8 | classDef unknownSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 9 | -------------------------------------------------------------------------------- /skillmap/themes/pale.theme: -------------------------------------------------------------------------------- 1 | classDef normalSkillGroup stroke-width:4px,stroke:#DCEBCA,fill:#E9F5DB; 2 | classDef beingLearnedSkill stroke-width:2px,stroke:#C2D5AA,fill:#CFE1B9; 3 | classDef learnedSkill stroke-width:2px,stroke:#A6B98B,fill:#B5C99A; 4 | 5 | classDef newSkillGroup stroke-width:4px,stroke:#D6CCC2,fill:#EDEDE9; 6 | classDef newSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 7 | classDef unknownSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 8 | 9 | %% https://www.w3schools.com/colors/colors_groups.asp 10 | linkStyle default stroke-width:2px,stroke:OliveDrab; -------------------------------------------------------------------------------- /skillmap/themes/rose.theme: -------------------------------------------------------------------------------- 1 | classDef normalSkillGroup stroke-width:4px,stroke:#FF97B7,fill:#FADDE1; 2 | classDef beingLearnedSkill stroke-width:2px,stroke:#FF87AB,fill:#FFC4D6; 3 | classDef learnedSkill stroke-width:2px,stroke:#FF5D8F,fill:#FF5D8F; 4 | 5 | classDef newSkillGroup stroke-width:4px,stroke:#D6CCC2,fill:#EDEDE9; 6 | classDef newSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 7 | classDef unknownSkill stroke-width:2px,stroke:#D6CCC2,fill:#EDEDE9; 8 | 9 | %% https://www.w3schools.com/colors/colors_groups.asp 10 | linkStyle default stroke-width:2px,stroke:PaleVioletRed; -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niyue/skillmap/ca1011d5f822134ad1d7c5f7f243da30a0731170/tests/__init__.py -------------------------------------------------------------------------------- /tests/nodes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niyue/skillmap/ca1011d5f822134ad1d7c5f7f243da30a0731170/tests/nodes/__init__.py -------------------------------------------------------------------------------- /tests/nodes/skill_node_test.py: -------------------------------------------------------------------------------- 1 | from skillmap.nodes.skill_node import create_skill_node, get_progress, SkillStatus 2 | from skillmap.nodes.progress_bar import PROGRESS_BAR_STYLES 3 | 4 | def test_get_0_progress(): 5 | progress, status = get_progress({}) 6 | assert progress == "" 7 | assert status == SkillStatus.NEW 8 | 9 | def test_get_fraction_progress(): 10 | progress, status = get_progress({"progress": "1/3"}) 11 | assert progress == "■□□" 12 | assert status == SkillStatus.BEING_LEANRED 13 | 14 | def test_get_finished_progress(): 15 | progress, status = get_progress({"progress": "3/3"}) 16 | assert progress == "■■■" 17 | assert status == SkillStatus.LEARNED 18 | 19 | def test_get_different_style_progress_bar(): 20 | progress, status = get_progress({"progress": "1/3"}, 1) 21 | assert progress == "█▁▁" 22 | assert status == SkillStatus.BEING_LEANRED 23 | 24 | def test_get_very_big_style_progress_bar(): 25 | progress, status = get_progress({"progress": "1/3"}, len(PROGRESS_BAR_STYLES) + 1) 26 | assert progress == "█▁▁" 27 | assert status == SkillStatus.BEING_LEANRED 28 | 29 | 30 | def test_get_0_fraction_progress(): 31 | progress, status = get_progress({"progress": "0/4"}) 32 | assert progress == "□□□□" 33 | assert status == SkillStatus.NEW 34 | 35 | def test_create_skill_node(): 36 | skill_graph = create_skill_node("s1", {"name": "url validator", "icon": "globe"}) 37 | sections = ['s1("fa:fa-globe
url validator")', "class s1 newSkill;", ""] 38 | assert skill_graph.split("\n") == sections 39 | 40 | 41 | def test_skill_node_with_progress(): 42 | skill_graph = create_skill_node( 43 | "s1", {"name": "url validator", "icon": "globe", "progress": "1/2"} 44 | ) 45 | sections = ['s1("fa:fa-globe
url validator
■□")', "class s1 beingLearnedSkill;", ""] 46 | assert skill_graph.split("\n") == sections 47 | 48 | 49 | def test_locked_create_skill_node(): 50 | skill_graph = create_skill_node("s1", {}) 51 | sections = ['s1("fa:fa-lock
???")', "class s1 unknownSkill;", ""] 52 | assert skill_graph.split("\n") == sections 53 | 54 | 55 | def test_skill_node_with_requires(): 56 | skill_graph = create_skill_node( 57 | "s1", {"name": "url validator", "icon": "globe", "requires": ["s2"]} 58 | ) 59 | sections = ['s1("fa:fa-globe
url validator")', "class s1 newSkill;", "s2-->s1"] 60 | assert skill_graph.split("\n") == sections 61 | -------------------------------------------------------------------------------- /tests/nodes/skillmap_node_test.py: -------------------------------------------------------------------------------- 1 | from skillmap.main import generate 2 | from skillmap.nodes.common import SECTION_SEPARATOR 3 | from skillmap.nodes.skillmap_node import create_skillmap_graph 4 | from skillmap.nodes.group_node import create_group_subgraph 5 | from skillmap.nodes.skill_node import create_skill_node 6 | 7 | 8 | def test_generate(): 9 | skillmap_file = "tests/url_shortener.toml" 10 | map_graph = generate(skillmap_file) 11 | assert map_graph 12 | assert "flowchart TD" in map_graph 13 | assert "url_shortener" in map_graph 14 | assert "url_shortener-->groups.backend" in map_graph 15 | assert "class groups.webui" in map_graph 16 | 17 | 18 | def test_skillmap_node_with_missing_name(): 19 | map_graph = create_skillmap_graph({"skillmap": {}}) 20 | assert map_graph 21 | assert "flowchart TD" in map_graph 22 | assert "unamed_skill_map" in map_graph 23 | 24 | 25 | def test_skillmap_node_with_missing_theme(): 26 | map_graph = create_skillmap_graph( 27 | { 28 | "skillmap": { 29 | "name": "url shortener", 30 | "icon": "anchor", 31 | "theme": "not_found", 32 | } 33 | } 34 | ) 35 | assert map_graph 36 | assert "flowchart TD" in map_graph 37 | assert "url shortener" in map_graph 38 | 39 | 40 | def test_skillmap_node_with_orientation(): 41 | map_graph = create_skillmap_graph( 42 | { 43 | "skillmap": { 44 | "name": "url shortener", 45 | "icon": "anchor", 46 | "orientation": "LR", 47 | } 48 | } 49 | ) 50 | assert map_graph 51 | assert "flowchart LR" in map_graph 52 | assert "url shortener" in map_graph 53 | 54 | 55 | def test_skillmap_node_with_auto_required_groups(): 56 | map_graph = create_skillmap_graph( 57 | { 58 | "skillmap": { 59 | "name": "url shortener", 60 | }, 61 | "groups": { 62 | "g1": { 63 | "name": "g1", 64 | }, 65 | "g2": { 66 | "name": "g2", 67 | "requires": ["g1"], 68 | }, 69 | }, 70 | } 71 | ) 72 | assert map_graph 73 | assert "flowchart TD" in map_graph 74 | assert "url_shortener-->groups.g1" in map_graph 75 | assert "url_shortener-->groups.g2" not in map_graph 76 | 77 | 78 | 79 | 80 | 81 | def test_visit_group_without_skill(): 82 | group_graph = create_group_subgraph( 83 | "g1", 84 | { 85 | "name": "web ui", 86 | "icon": "anchor", 87 | }, 88 | ) 89 | sections = [ 90 | SECTION_SEPARATOR, 91 | 'subgraph groups.g1["fa:fa-anchor web ui"]', 92 | "", # skill list is skipped 93 | "end", 94 | "class groups.g1 newSkillGroup;", 95 | "", 96 | ] 97 | assert group_graph.split("\n") == sections 98 | 99 | 100 | def test_visit_group(): 101 | group_graph = create_group_subgraph( 102 | "g1", 103 | { 104 | "name": "web ui", 105 | "icon": "anchor", 106 | "skills": { 107 | "s1": {"name": "url validator", "icon": "globe"}, 108 | "s2": {"name": "React", "icon": "html5"}, 109 | }, 110 | }, 111 | ) 112 | sections = [ 113 | SECTION_SEPARATOR, 114 | 'subgraph groups.g1["fa:fa-anchor web ui"]', 115 | 'groups.g1.skills.s1("fa:fa-globe
url validator")', 116 | "class groups.g1.skills.s1 newSkill;", 117 | "", 118 | 'groups.g1.skills.s2("fa:fa-html5
React")', 119 | "class groups.g1.skills.s2 newSkill;", 120 | "", 121 | "end", 122 | "class groups.g1 newSkillGroup;", 123 | "", 124 | ] 125 | assert group_graph.split("\n") == sections 126 | 127 | 128 | def test_visit_group_with_requires(): 129 | group_graph = create_group_subgraph( 130 | "g1", 131 | { 132 | "name": "web ui", 133 | "icon": "anchor", 134 | "requires": ["groups.g2.skills.s1"], 135 | }, 136 | ) 137 | sections = [ 138 | SECTION_SEPARATOR, 139 | 'subgraph groups.g1["fa:fa-anchor web ui"]', 140 | "", # skill list is skipped 141 | "end", 142 | "class groups.g1 newSkillGroup;", 143 | "groups.g2.skills.s1-->groups.g1", 144 | ] 145 | assert group_graph.split("\n") == sections 146 | -------------------------------------------------------------------------------- /tests/skillmap_parser_test.py: -------------------------------------------------------------------------------- 1 | from skillmap.skillmap_parser import SkillMapParser 2 | 3 | 4 | def test_parse_toml(): 5 | parser = SkillMapParser() 6 | skill_map = parser.parse('tests/url_shortener.toml') 7 | assert skill_map 8 | assert skill_map['skillmap']['name'] == "url shortener" 9 | assert skill_map['groups']['webui']['name'] == "web ui" 10 | assert skill_map['groups']['webui']['skills']['url_validator']['name'] == "url validator" 11 | assert skill_map['groups']['webui']['skills']['url_validator']['icon'] == "globe" 12 | -------------------------------------------------------------------------------- /tests/url_shortener.toml: -------------------------------------------------------------------------------- 1 | [skillmap] 2 | name = "url shortener" 3 | icon = "hashtag" 4 | # theme = "pale" 5 | # orientation = "LR" 6 | # progress_bar_style = 16 7 | 8 | [groups.webui] 9 | name = "web ui" 10 | icon = "desktop" 11 | [groups.webui.skills.url_validator] 12 | name = "url validator" 13 | icon = "globe" 14 | requires = ["groups.webui.skills.react"] 15 | progress = "0/3" 16 | [groups.webui.skills.react] 17 | name = "react" 18 | icon = "list" 19 | status = "beingLearned" 20 | progress = "1/3" 21 | 22 | [groups.native_client] 23 | name = "native client" 24 | icon = "desktop" 25 | requires = ["groups.webui"] 26 | [groups.native_client.skills.react_native] 27 | name = "react native" 28 | icon = "mobile" 29 | [groups.native_client.skills.other_clients] 30 | 31 | [groups.backend] 32 | name = "backend" 33 | icon = "server" 34 | [groups.backend.skills.restapi] 35 | name = "REST API" 36 | icon = "send" 37 | status = "learned" 38 | progress = "3/3" 39 | [groups.backend.skills.database] 40 | name = "database" 41 | icon = "database" 42 | status = "learned" 43 | progress = "3/3" 44 | 45 | [groups.partitioned_backend] 46 | name = "partitioned backend" 47 | icon = "copy" 48 | requires = ["groups.backend"] 49 | [groups.partitioned_backend.skills.proxy] 50 | name = "proxy" 51 | icon = "anchor" 52 | status = "beingLearned" 53 | progress = "2/3" 54 | [groups.partitioned_backend.skills.database] 55 | name = "partitioned database" 56 | icon = "database" 57 | status = "beingLearned" 58 | 59 | [groups.cache] 60 | name = "cache" 61 | icon = "archive" 62 | [groups.cache.skills.memcache] 63 | name = "memcache" 64 | icon = "magnet" 65 | status = "learned" 66 | [groups.cache.skills.redis] --------------------------------------------------------------------------------