├── .gitignore ├── LICENSE ├── README.md ├── img ├── increment_pages.png ├── increment_topnav.png ├── increment_topnavANDpags.png ├── none_strict_mode.png └── strict_mode.png ├── mkdocs_add_number_plugin ├── __init__.py ├── markdown.py ├── plugin.py └── utils.py ├── setup.py └── tests ├── dummy_project ├── docs │ ├── a_third_page.md │ ├── first_page.md │ ├── index.md │ └── second_page.md ├── mkdocs.yml ├── mkdocs_with_excludes.yml ├── mkdocs_with_nav.yml └── mkdocs_with_strict.yml ├── test_builds.py ├── test_flatten.py ├── test_markdown.py └── test_requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | # Created by https://www.gitignore.io/api/r,macos,python,pycharm,windows,jupyternotebooks,visualstudiocode 4 | # Edit at https://www.gitignore.io/?templates=r,macos,python,pycharm,windows,jupyternotebooks,visualstudiocode 5 | 6 | ### JupyterNotebooks ### 7 | # gitignore template for Jupyter Notebooks 8 | # website: http://jupyter.org/ 9 | 10 | .ipynb_checkpoints 11 | */.ipynb_checkpoints/* 12 | 13 | # IPython 14 | profile_default/ 15 | ipython_config.py 16 | 17 | # Remove previous ipynb_checkpoints 18 | # git rm -r .ipynb_checkpoints/ 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### PyCharm ### 49 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 50 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 51 | 52 | # User-specific stuff 53 | .idea/**/workspace.xml 54 | .idea/**/tasks.xml 55 | .idea/**/usage.statistics.xml 56 | .idea/**/dictionaries 57 | .idea/**/shelf 58 | 59 | # Generated files 60 | .idea/**/contentModel.xml 61 | 62 | # Sensitive or high-churn files 63 | .idea/**/dataSources/ 64 | .idea/**/dataSources.ids 65 | .idea/**/dataSources.local.xml 66 | .idea/**/sqlDataSources.xml 67 | .idea/**/dynamic.xml 68 | .idea/**/uiDesigner.xml 69 | .idea/**/dbnavigator.xml 70 | 71 | # Gradle 72 | .idea/**/gradle.xml 73 | .idea/**/libraries 74 | 75 | # Gradle and Maven with auto-import 76 | # When using Gradle or Maven with auto-import, you should exclude module files, 77 | # since they will be recreated, and may cause churn. Uncomment if using 78 | # auto-import. 79 | # .idea/modules.xml 80 | # .idea/*.iml 81 | # .idea/modules 82 | # *.iml 83 | # *.ipr 84 | 85 | # CMake 86 | cmake-build-*/ 87 | 88 | # Mongo Explorer plugin 89 | .idea/**/mongoSettings.xml 90 | 91 | # File-based project format 92 | *.iws 93 | 94 | # IntelliJ 95 | out/ 96 | 97 | # mpeltonen/sbt-idea plugin 98 | .idea_modules/ 99 | 100 | # JIRA plugin 101 | atlassian-ide-plugin.xml 102 | 103 | # Cursive Clojure plugin 104 | .idea/replstate.xml 105 | 106 | # Crashlytics plugin (for Android Studio and IntelliJ) 107 | com_crashlytics_export_strings.xml 108 | crashlytics.properties 109 | crashlytics-build.properties 110 | fabric.properties 111 | 112 | # Editor-based Rest Client 113 | .idea/httpRequests 114 | 115 | # Android studio 3.1+ serialized cache file 116 | .idea/caches/build_file_checksums.ser 117 | 118 | ### PyCharm Patch ### 119 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 120 | 121 | # *.iml 122 | # modules.xml 123 | # .idea/misc.xml 124 | # *.ipr 125 | 126 | # Sonarlint plugin 127 | .idea/**/sonarlint/ 128 | 129 | # SonarQube Plugin 130 | .idea/**/sonarIssues.xml 131 | 132 | # Markdown Navigator plugin 133 | .idea/**/markdown-navigator.xml 134 | .idea/**/markdown-navigator/ 135 | 136 | ### Python ### 137 | # Byte-compiled / optimized / DLL files 138 | __pycache__/ 139 | *.py[cod] 140 | *$py.class 141 | 142 | # C extensions 143 | *.so 144 | 145 | # Distribution / packaging 146 | .Python 147 | build/ 148 | develop-eggs/ 149 | dist/ 150 | downloads/ 151 | eggs/ 152 | .eggs/ 153 | lib/ 154 | lib64/ 155 | parts/ 156 | sdist/ 157 | var/ 158 | wheels/ 159 | pip-wheel-metadata/ 160 | share/python-wheels/ 161 | *.egg-info/ 162 | .installed.cfg 163 | *.egg 164 | MANIFEST 165 | 166 | # PyInstaller 167 | # Usually these files are written by a python script from a template 168 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 169 | *.manifest 170 | *.spec 171 | 172 | # Installer logs 173 | pip-log.txt 174 | pip-delete-this-directory.txt 175 | 176 | # Unit test / coverage reports 177 | htmlcov/ 178 | .tox/ 179 | .nox/ 180 | .coverage 181 | .coverage.* 182 | .cache 183 | nosetests.xml 184 | coverage.xml 185 | *.cover 186 | .hypothesis/ 187 | .pytest_cache/ 188 | 189 | # Translations 190 | *.mo 191 | *.pot 192 | 193 | # Scrapy stuff: 194 | .scrapy 195 | 196 | # Sphinx documentation 197 | docs/_build/ 198 | 199 | # PyBuilder 200 | target/ 201 | 202 | # pyenv 203 | .python-version 204 | 205 | # pipenv 206 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 207 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 208 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 209 | # install all needed dependencies. 210 | #Pipfile.lock 211 | 212 | # celery beat schedule file 213 | celerybeat-schedule 214 | 215 | # SageMath parsed files 216 | *.sage.py 217 | 218 | # Spyder project settings 219 | .spyderproject 220 | .spyproject 221 | 222 | # Rope project settings 223 | .ropeproject 224 | 225 | # Mr Developer 226 | .mr.developer.cfg 227 | .project 228 | .pydevproject 229 | 230 | # mkdocs documentation 231 | /site 232 | 233 | # mypy 234 | .mypy_cache/ 235 | .dmypy.json 236 | dmypy.json 237 | 238 | # Pyre type checker 239 | .pyre/ 240 | 241 | ### R ### 242 | # History files 243 | .Rhistory 244 | .Rapp.history 245 | 246 | # Session Data files 247 | .RData 248 | .RDataTmp 249 | 250 | # User-specific files 251 | .Ruserdata 252 | 253 | # Example code in package build process 254 | *-Ex.R 255 | 256 | # Output files from R CMD build 257 | /*.tar.gz 258 | 259 | # Output files from R CMD check 260 | /*.Rcheck/ 261 | 262 | # RStudio files 263 | .Rproj.user/ 264 | 265 | # produced vignettes 266 | vignettes/*.html 267 | vignettes/*.pdf 268 | 269 | # OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 270 | .httr-oauth 271 | 272 | # knitr and R markdown default cache directories 273 | *_cache/ 274 | /cache/ 275 | 276 | # Temporary files created by R markdown 277 | *.utf8.md 278 | *.knit.md 279 | 280 | ### R.Bookdown Stack ### 281 | # R package: bookdown caching files 282 | /*_files/ 283 | 284 | ### VisualStudioCode ### 285 | .vscode/* 286 | !.vscode/settings.json 287 | !.vscode/tasks.json 288 | !.vscode/launch.json 289 | !.vscode/extensions.json 290 | 291 | ### VisualStudioCode Patch ### 292 | # Ignore all local history of files 293 | .history 294 | 295 | ### Windows ### 296 | # Windows thumbnail cache files 297 | Thumbs.db 298 | Thumbs.db:encryptable 299 | ehthumbs.db 300 | ehthumbs_vista.db 301 | 302 | # Dump file 303 | *.stackdump 304 | 305 | # Folder config file 306 | [Dd]esktop.ini 307 | 308 | # Recycle Bin used on file shares 309 | $RECYCLE.BIN/ 310 | 311 | # Windows Installer files 312 | *.cab 313 | *.msi 314 | *.msix 315 | *.msm 316 | *.msp 317 | 318 | # Windows shortcuts 319 | *.lnk 320 | 321 | # End of https://www.gitignore.io/api/r,macos,python,pycharm,windows,jupyternotebooks,visualstudiocode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 ignorantshr 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mkdocs-add-number-plugin) 2 | ![PyPI](https://img.shields.io/pypi/v/mkdocs-add-number-plugin) 3 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/mkdocs-add-number-plugin) 4 | ![GitHub contributors](https://img.shields.io/github/contributors/timvink/mkdocs-add-number-plugin) 5 | ![PyPI - License](https://img.shields.io/pypi/l/mkdocs-add-number-plugin) 6 | 7 | # mkdocs-add-number-plugin 8 | 9 | [MkDocs](https://www.mkdocs.org/) plugin to automatically number the headings (h1-h6) in each markdown page and the nav. This only affects your rendered HTML and does not affect the markdown files. 10 | 11 | ## Setup 12 | 13 | ### use pip3 14 | Install the plugin using pip3: 15 | 16 | ```bash 17 | pip3 install mkdocs-add-number-plugin 18 | ``` 19 | 20 | ### build from source 21 | use git clone the source code to your computer and execute commands: 22 | 23 | ```shell 24 | cd mkdocs-add-number-plugin 25 | mkdir wheels 26 | cd wheels 27 | # if you have installed the plugin, uninstall it. 28 | # pip3 uninstall mkdocs-add-number-plugin -y 29 | pip3 wheel .. 30 | pip3 install mkdocs_add_number_plugin-*-py3-none-any.whl 31 | ``` 32 | 33 | Next, add the following lines to your `mkdocs.yml`: 34 | 35 | ```yml 36 | plugins: 37 | - search 38 | - add-number 39 | ``` 40 | 41 | > If you have no `plugins` entry in your config file yet, you'll likely also want to add the `search` plugin. MkDocs enables it by default if there is no `plugins` entry set. 42 | 43 | ## Usage 44 | 45 | Example of multiple options set in the `mkdocs.yml` file: 46 | 47 | ```yml 48 | plugins: 49 | - search 50 | - add-number: 51 | strict_mode: False 52 | order: 1 53 | excludes: 54 | - sql/ 55 | - command/rsync 56 | includes: 57 | - sql/MySQL 58 | ``` 59 | 60 | > Note that each title in your markdown page must be contain a space after the heading tag: `# title` is correct, but `#title` will cause an error. 61 | 62 | ## Options 63 | 64 | - `strict_mode`: 65 | - When set to `False` (default), orders the title numbers in your HTML page sequentially 66 | - When set to `True` it will follow the headings order strictly. You must start writing documents from h1, and cannot skip headings (such as `# title1\n### title2\n`). 67 | - `order`: Heading level to start counting (number). Default is `1`. Example use case: When you want to use the first title of a document as the title of a document and you don't want to number it, set it to `order: 2`. 68 | - `excludes`: Exclude certain files or folders in the `docs/` folder. Default is None. To exclude entire folders, append a slash (`folder/`). 69 | - `includes`: The syntax is similar to `excludes` and it meant to be used together. You could for example exclude an entire folder but include several files from that folder. 70 | - `increment_topnav`: Number top-level navigation. 71 | - `increment_pages`: Number secondary navigation. 72 | 73 | 74 | ### Example of using `excludes` 75 | 76 | For example, there is a mkdocs directory structure as follows: 77 | 78 | ```shell 79 | $ tree . 80 | . 81 | ├── docs 82 | │ ├── command 83 | │ │ ├── rsync.md 84 | │ │ ├── sed.md 85 | | ... 86 | └── mkdocs.yml 87 | ``` 88 | 89 | To exclude rsync file, add the excludes option as follows: 90 | 91 | ```yaml 92 | plugins: 93 | - search 94 | - add-number: 95 | excludes: 96 | - command/rsync 97 | ``` 98 | 99 | If you want to exclude the command folder, add the excludes option as follows: 100 | 101 | ```yaml 102 | plugins: 103 | - search 104 | - add-number: 105 | excludes: 106 | - command/ 107 | ``` 108 | 109 | ### Example of using `increment_topnav` 110 | 111 | Number top-level navigation : 112 | 113 | ```yaml 114 | increment_topnav: True|False 115 | ``` 116 | 117 | The deault value is `False`. 118 | **note**: Both `includes` and `excludes` options don't affect this option. 119 | 120 | Effect after enabling: 121 | 122 | ![](img/increment_topnav.png) 123 | 124 | 125 | ### Example of using `increment_pages` 126 | 127 | Number secondary navigation : 128 | 129 | ```yaml 130 | increment_pages: True|False 131 | ``` 132 | 133 | The deault value is `False`. 134 | **note**: Both `includes` and `excludes` options don't affect this option. 135 | 136 | Effect after enabling: 137 | 138 | ![](img/increment_pages.png) 139 | 140 | 141 | ### Example of using `increment_topnav `with `increment_pages` 142 | 143 | When both are turned on at the same time, the numbering effect of the secondary navigation is affected 144 | 145 | ![](img/increment_topnavANDpags.png) 146 | 147 | 148 | ------ CHINESE VERSION ------ 149 | 150 | # mkdocs-add-number-plugin 151 | 152 | 一个mkdocs插件:为你的每个页面的标题(h1~h6)自动编号。**这只影响你的html渲染结果,并不影响markdown文档本身!** 153 | 154 | ## 安装 155 | 156 | ### 使用 pip 安装 157 | 158 | ```shell 159 | # if you have installed the plugin, uninstall it. 160 | # pip3 uninstall mkdocs-add-number-plugin -y 161 | pip3 install mkdocs-add-number-plugin 162 | ``` 163 | 164 | ### 从源码构建安装 165 | 克隆此项目到你的计算机上,然后执行以下命令: 166 | 167 | ```shell 168 | cd mkdocs-add-number-plugin 169 | mkdir wheels 170 | cd wheels 171 | # if you have installed the plugin, uninstall it. 172 | # pip3 uninstall mkdocs-add-number-plugin -y 173 | pip3 wheel .. 174 | pip3 install mkdocs_add_number_plugin-*-py3-none-any.whl 175 | ``` 176 | 177 | ## 使用 178 | 179 | 在`mkdocs.yml`文件中的`plugins`选项添加此插件: 180 | 181 | ```yaml 182 | plugins: 183 | - search 184 | - add-number: 185 | strict_mode: False 186 | order: 1 187 | excludes: 188 | - sql/ 189 | - command/rsync 190 | includes: 191 | - sql/MySQL 192 | ``` 193 | 194 | 195 | ## 提供的选项 196 | 197 | - `strict_mode` 198 | - `order` 199 | - `excludes` 200 | - `includes` 201 | - `increment_topnav` 202 | - `increment_pages` 203 | 204 | ### strict_mode 205 | 206 | 指定编号的模式。 207 | 208 | 语法: 209 | 210 | ```yaml 211 | strict_mode: True|False 212 | ``` 213 | 214 | 2. True:严格模式。顺序地为你的html页面的标题编号。必须从h1开始撰写文档,且不能有跳级(比如`# title1\n### title2\n`,*title2*不会被编号,可以选用非严格模式为其编号),但是可以不必用到所有级数。 215 | 2. False:非严格模式(默认值)。顺序地为你的html页面的标题编号。没有上述的限制。 216 | 217 | **注意**:两种模式的标题级数都不能有倒序出现。比如`### title1\n# title2\n`,这会导致编号异常。并且每个标题后面都要有空格与文字隔开,比如这样`# title`是正确的,而这样`#title`是不行的。 218 | 219 | 220 | #### 效果 221 | 222 | 非严格模式效果: 223 | 224 | ![](img/none_strict_mode.png) 225 | 226 | 严格模式效果: 227 | 228 | ![](img/strict_mode.png) 229 | 230 | 231 | ### order 232 | 233 | 从第几个标题开始编号。在某些场景下是有用的:你想要将文档的第一个标题作为文档的题目而不想对其进行编号时,设置为`order: 2`。 234 | 235 | 语法: 236 | 237 | ```yaml 238 | order: 数字 239 | ``` 240 | 241 | 数字必须是大于1的自然数,默认值是1。 242 | 243 | 244 | ### excludes 245 | 246 | 排除某些文件或文件夹。 247 | 248 | 语法: 249 | 250 | ```yaml 251 | excludes: 列表|'*' 252 | ``` 253 | 254 | - 列表:遵循`yaml`文件的列表语法,文件或文件夹填写`docs`文件夹下的相对路径,不必填写文件后缀。**以`/`或`\`结尾的值表示文件夹**。 255 | - '*':表示排除所有的文件。因为默认值为空列表`[]`,意味着对所有的文件进行编号,所以你需要使用此值来不对所有的文件进行编号。 256 | 257 | ##### 例子 258 | 259 | 比如现在有一 mkdocs 目录结构如下: 260 | 261 | ```shell 262 | $ tree . 263 | . 264 | ├── docs 265 | │   ├── command 266 | │   │   ├── rsync.md 267 | │   │   ├── sed.md 268 | | ... 269 | └── mkdocs.yml 270 | ``` 271 | 272 | 想要排除rsync文件,添加的excludes选项如下: 273 | 274 | ```yaml 275 | plugins: 276 | - search 277 | - add-number: 278 | excludes: 279 | - command\rsync 280 | ``` 281 | 282 | 若想要排除command文件夹,添加的excludes选项如下: 283 | 284 | ```yaml 285 | plugins: 286 | - search 287 | - add-number: 288 | excludes: 289 | - command\ 290 | ``` 291 | 292 | 293 | ### includes 294 | 295 | 包含某些文件或文件夹。 296 | 297 | 语法与`excludes`类似: 298 | 299 | ```yaml 300 | includes: 列表 301 | ``` 302 | 303 | 在被`excludes`排除的文件或文件夹如果在`includes`选项中出现,那么会对其进行编号。默认值为空列表`[]`。 304 | 305 | **注意**:includes是作为excludes的辅助选项使用的(意味着必须和excludes一起使用,单独使用此选项没有意义)。 306 | 307 | ### increment_topnav 308 | 309 | 对顶级目录索引进行编号。 310 | 311 | 语法: 312 | 313 | ```yaml 314 | increment_topnav: True|False 315 | ``` 316 | 317 | 默认值为 False。 318 | **注意**:`includes`和`excludes`选项不会影响此选项。 319 | 320 | 开启之后的效果: 321 | 322 | ![](img/increment_topnav.png) 323 | 324 | 325 | ### increment_pages 326 | 327 | 对二级目录索引进行编号。 328 | 329 | 语法: 330 | 331 | ```yaml 332 | increment_pages: True|False 333 | ``` 334 | 335 | 默认值为 False。 336 | **注意**:`includes`和`excludes`选项不会影响此选项。 337 | 338 | 开启之后的效果: 339 | 340 | ![](img/increment_pages.png) 341 | 342 | 343 | ### increment_topnav 与 increment_pages 共用 344 | 345 | 两者同时开启时会影响二级目录索引的编号效果: 346 | 347 | ![](img/increment_topnavANDpags.png) 348 | 349 | -------------------------------------------------------------------------------- /img/increment_pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ignorantshr/mkdocs-add-number-plugin/e77b4f21b17603c2cf201e86bcfb00cd06509591/img/increment_pages.png -------------------------------------------------------------------------------- /img/increment_topnav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ignorantshr/mkdocs-add-number-plugin/e77b4f21b17603c2cf201e86bcfb00cd06509591/img/increment_topnav.png -------------------------------------------------------------------------------- /img/increment_topnavANDpags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ignorantshr/mkdocs-add-number-plugin/e77b4f21b17603c2cf201e86bcfb00cd06509591/img/increment_topnavANDpags.png -------------------------------------------------------------------------------- /img/none_strict_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ignorantshr/mkdocs-add-number-plugin/e77b4f21b17603c2cf201e86bcfb00cd06509591/img/none_strict_mode.png -------------------------------------------------------------------------------- /img/strict_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ignorantshr/mkdocs-add-number-plugin/e77b4f21b17603c2cf201e86bcfb00cd06509591/img/strict_mode.png -------------------------------------------------------------------------------- /mkdocs_add_number_plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ignorantshr/mkdocs-add-number-plugin/e77b4f21b17603c2cf201e86bcfb00cd06509591/mkdocs_add_number_plugin/__init__.py -------------------------------------------------------------------------------- /mkdocs_add_number_plugin/markdown.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List 3 | from collections import OrderedDict 4 | 5 | 6 | def headings(lines: List[str]): 7 | """Findings lines that are markdown headings 8 | 9 | Args: 10 | lines (list): List with lines (strings) 11 | 12 | Returns: 13 | headings (dict): line number (key) and line (str) 14 | """ 15 | heading_lines = OrderedDict({}) 16 | is_block = False 17 | n = 0 18 | while n < len(lines): 19 | if lines[n].startswith('```'): 20 | is_block = not is_block 21 | 22 | if not is_block and lines[n].startswith('#'): 23 | heading_lines[n] = lines[n] 24 | n += 1 25 | 26 | return heading_lines 27 | 28 | 29 | def heading_depth(line): 30 | """Returns depth of heading indent 31 | 32 | '# heading' returns 1 33 | '### heading' returns 3 34 | 35 | Args: 36 | line (str): line in a markdown page 37 | """ 38 | assert line.startswith('#') 39 | n = 0 40 | while line[n:n + 1] == '#': 41 | n += 1 42 | 43 | return n 44 | 45 | 46 | def update_heading_chapter(line: str, chapter: int): 47 | pattern = re.compile(r"(^[\#].+)(1)(\..+)") 48 | matches = pattern.match(line) 49 | if not matches: 50 | return line 51 | else: 52 | return "%s%s%s" % ( 53 | matches.group(1), 54 | str(chapter), 55 | matches.group(3) 56 | ) 57 | -------------------------------------------------------------------------------- /mkdocs_add_number_plugin/plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from mkdocs.config import config_options 4 | from mkdocs.plugins import BasePlugin 5 | from mkdocs.structure.nav import Section 6 | from mkdocs.structure.pages import Page 7 | 8 | from .utils import flatten 9 | from . import markdown as md 10 | 11 | 12 | class AddNumberPlugin(BasePlugin): 13 | config_scheme = ( 14 | ('strict_mode', config_options.Type(bool, default=False)), 15 | ('increment_pages', config_options.Type(bool, default=False)), 16 | ('increment_topnav', config_options.Type(bool, default=False)), 17 | ('excludes', config_options.Type(list, default=[])), 18 | ('includes', config_options.Type(list, default=[])), 19 | ('order', config_options.Type(int, default=1)) 20 | ) 21 | 22 | def _check_config_params(self): 23 | set_parameters = self.config.keys() 24 | allowed_parameters = dict(self.config_scheme).keys() 25 | if set_parameters != allowed_parameters: 26 | unknown_parameters = [x for x in set_parameters if 27 | x not in allowed_parameters] 28 | raise AssertionError( 29 | "Unknown parameter(s) set: %s" % ", ".join(unknown_parameters)) 30 | 31 | def on_nav(self, nav, config, files): 32 | """ 33 | The nav event is called after the site navigation is created and 34 | can be used to alter the site navigation. 35 | 36 | See: 37 | https://www.mkdocs.org/user-guide/plugins/#on_nav 38 | 39 | :param nav: global navigation object 40 | :param config: global configuration object 41 | :param files: global files collection 42 | :return: global navigation object 43 | """ 44 | self._title2index = dict() 45 | is_increment_topnav = self.config.get("increment_topnav", False) 46 | is_increment_pages = self.config.get("increment_pages", False) 47 | 48 | index = 0 49 | while index < len(nav.items): 50 | if is_increment_topnav: 51 | nav.items[index].title = str(index + 1) + '. ' + \ 52 | nav.items[index].title 53 | # Section(title='Linux') 54 | # Page(title=[blank], url='/linux/epel%E6%BA%90/') 55 | if type(nav.items[index]) == Section: 56 | pages = nav.items[index].children 57 | j = 0 58 | while j < len(pages): 59 | if is_increment_topnav and is_increment_pages: 60 | self._title2index[pages[j].url] = \ 61 | str(index + 1) + '.' + str(j + 1) + ' ' 62 | elif is_increment_pages: 63 | self._title2index[pages[j].url] = str(j + 1) + '. ' 64 | j += 1 65 | index += 1 66 | return nav 67 | 68 | def on_files(self, files, config): 69 | """ 70 | The files event is called after the files collection is populated from the docs_dir. 71 | Use this event to add, remove, or alter files in the collection. 72 | 73 | See https://www.mkdocs.org/user-guide/plugins/#on_files 74 | 75 | Args: 76 | files (list): files: global files collection 77 | config (dict): global configuration object 78 | 79 | Returns: 80 | files (list): global files collection 81 | """ 82 | self._check_config_params() 83 | 84 | # Use navigation if set, 85 | # (see https://www.mkdocs.org/user-guide/configuration/#nav) 86 | # only these files will be displayed. 87 | nav = config.get('nav', None) 88 | if nav: 89 | files_str = flatten(nav) 90 | # Otherwise, take all source markdown pages 91 | else: 92 | files_str = [ 93 | file.src_path for file in files if file.is_documentation_page() 94 | ] 95 | 96 | # Record excluded files from selection by user 97 | self._excludes = self.config['excludes'] 98 | self._exclude_files = [os.path.normpath(file1) for file1 in 99 | self._excludes if not file1.endswith('\\') 100 | and not file1.endswith('/')] 101 | self._exclude_dirs = [os.path.normpath(dir1) for dir1 in self._excludes 102 | if dir1.endswith('\\') 103 | or dir1.endswith('/')] 104 | 105 | self._includes = self.config['includes'] 106 | self._include_files = [os.path.normpath(file1) for file1 in 107 | self._includes if not file1.endswith('\\') 108 | and not file1.endswith('/')] 109 | self._include_dirs = [os.path.normpath(dir1) for dir1 in self._includes 110 | if dir1.endswith('\\') 111 | or dir1.endswith('/')] 112 | 113 | self._order = self.config['order'] - 1 114 | 115 | # Remove files excluded from selection by user 116 | files_to_remove = [file for file in files_str if 117 | self._is_exclude(file) and not self._is_include( 118 | file)] 119 | self.files_str = [file for file in files_str if 120 | file not in files_to_remove] 121 | 122 | return files 123 | 124 | def on_page_markdown(self, markdown, page, config, files): 125 | """ 126 | The page_markdown event is called after the page's markdown is loaded 127 | from file and can be used to alter the Markdown source text. 128 | The meta- data has been stripped off and is available as page.meta 129 | at this point. 130 | 131 | See: 132 | https://www.mkdocs.org/user-guide/plugins/#on_page_markdown 133 | 134 | Args: 135 | markdown (str): Markdown source text of page as string 136 | page (Page): mkdocs.nav.Page instance 137 | config (dict): global configuration object 138 | files (list): global files collection 139 | 140 | Returns: 141 | markdown (str): Markdown source text of page as string 142 | """ 143 | if self.config.get('increment_pages', False): 144 | index_str = self._title2index.get(page.url, None) 145 | if index_str: 146 | page.title = index_str + page.title 147 | 148 | if page.file.src_path not in self.files_str: 149 | return markdown 150 | 151 | lines = markdown.split('\n') 152 | heading_lines = md.headings(lines) 153 | 154 | if len(heading_lines) <= self._order: 155 | return markdown 156 | 157 | tmp_lines_values = list(heading_lines.values()) 158 | 159 | if self.config['strict_mode']: 160 | tmp_lines_values, _ = self._searchN(tmp_lines_values, 1, 161 | self._order, 1, []) 162 | else: 163 | tmp_lines_values = self._ascent(tmp_lines_values, [0], 0, [], 1, 164 | self._order) 165 | 166 | # replace the links of current page after numbering the titles 167 | def _format_link_line(line): 168 | line = line.replace(".", "") 169 | new_line = '' 170 | for s in line: 171 | if s.isdigit() or s in (" ", "_") \ 172 | or (u'\u0041' <= s <= u'\u005a') \ 173 | or (u'\u0061' <= s <= u'\u007a'): 174 | new_line += s.lower() 175 | return '#' + '-'.join(new_line.split()) 176 | 177 | link_lines = [_format_link_line(v) for v in tmp_lines_values] 178 | link_lines = {'#' + i.split("-", 1)[1]: i for i in link_lines 179 | if i.count('-') > 0} 180 | n = 0 181 | while n < len(lines): 182 | for k in link_lines.keys(): 183 | line_content = lines[n] 184 | if line_content.count('[') >= 1 \ 185 | and line_content.count('(') >= 1: 186 | lines[n] = line_content.replace(k, link_lines[k]) 187 | n += 1 188 | 189 | # replace these new titles 190 | n = 0 191 | for key in heading_lines.keys(): 192 | lines[key] = tmp_lines_values[n] 193 | n += 1 194 | 195 | return '\n'.join(lines) 196 | 197 | def _ascent(self, tmp_lines, parent_nums_head, level, args, num, startrow): 198 | """ 199 | Add number to every line. 200 | 201 | e.g. 202 | if number from h2, then the level is: 203 | ## level=1 204 | ### level=2 205 | #### level=3 206 | ### level=2 207 | 208 | args 209 | |...| 210 | v v 211 | ###### 212 | ^ 213 | | 214 | num 215 | 216 | :param tmp_lines: line 217 | :param parent_nums_head: storage depth of header before this line. 218 | :param level: level of header 219 | :param args: all of numbers to combine the number 220 | :param num: the last number 221 | :param startrow: start row to deal 222 | :return: lines which has been numbered 223 | """ 224 | if startrow == len(tmp_lines): 225 | return tmp_lines 226 | 227 | nums_head = md.heading_depth(tmp_lines[startrow]) 228 | parent_nums = parent_nums_head[len(parent_nums_head) - 1] 229 | chang_num = nums_head - parent_nums 230 | 231 | # drop one level 232 | if chang_num < 0: 233 | if level != 1: 234 | # for _ in range(-chang_num): 235 | num = args.pop() 236 | level -= 1 237 | parent_nums_head.pop() 238 | return self._ascent(tmp_lines, parent_nums_head, level, args, num, 239 | startrow) 240 | 241 | # sibling 242 | if chang_num == 0: 243 | num += 1 244 | tmp_lines[startrow] = self._replace_line(tmp_lines[startrow], 245 | '#' * nums_head + ' ', 246 | '%d.' * len(args) % tuple( 247 | args), num) 248 | return self._ascent(tmp_lines, parent_nums_head, level, args, num, 249 | startrow + 1) 250 | 251 | # rise one level 252 | level += 1 253 | if level != 1: 254 | # for _ in range(chang_num): 255 | args.append(num) 256 | parent_nums_head.append(nums_head) 257 | num = 1 258 | tmp_lines[startrow] = self._replace_line(tmp_lines[startrow], 259 | '#' * nums_head + ' ', 260 | '%d.' * len(args) % tuple( 261 | args), num) 262 | return self._ascent(tmp_lines, parent_nums_head, level, args, num, 263 | startrow + 1) 264 | 265 | def _replace_line(self, tmp_line, substr, prenum_str, nextnum): 266 | re_str = (substr + "%d. " % nextnum) if (prenum_str == '') else ( 267 | substr + "%s%d " % (prenum_str, nextnum)) 268 | tmp_line = tmp_line.replace(substr, re_str) 269 | return tmp_line 270 | 271 | def _searchN(self, tmp_lines, num, start_row, level, args): 272 | while True: 273 | tmp_lines, start_row, re = self._replace(tmp_lines, 274 | '#' * level + ' ', 275 | '.'.join(('%d.' * ( 276 | level - 1)).split()) % tuple( 277 | args), 278 | num, start_row) 279 | if not re: 280 | break 281 | 282 | next_num = 1 283 | if level != 6: 284 | args.append(num) 285 | re_lines, start_row = self._searchN(tmp_lines, next_num, 286 | start_row, level + 1, args) 287 | args.pop() 288 | 289 | num += 1 290 | 291 | return tmp_lines, start_row 292 | 293 | def _replace(self, tmp_lines, substr, prenum_str, nextnum, start_row): 294 | if start_row == len(tmp_lines) or not tmp_lines[start_row].startswith( 295 | substr): 296 | return tmp_lines, start_row, False 297 | 298 | re_str = (substr + "%d. " % nextnum) if (prenum_str == '') else ( 299 | substr + "%s%d " % (prenum_str, nextnum)) 300 | tmp_lines[start_row] = tmp_lines[start_row].replace(substr, re_str) 301 | return tmp_lines, start_row + 1, True 302 | 303 | def _is_exclude(self, file): 304 | if len(self._excludes) == 0: 305 | return False 306 | 307 | url = os.path.normpath(file) 308 | 309 | if url in self._exclude_files or '*' in self._exclude_files: 310 | return True 311 | 312 | for dir1 in self._exclude_dirs: 313 | if url.find(dir1) != -1: 314 | return True 315 | 316 | return False 317 | 318 | def _is_include(self, file): 319 | if len(self._includes) == 0: 320 | return False 321 | 322 | url = os.path.normpath(file) 323 | 324 | if url in self._include_files: 325 | return True 326 | 327 | for dir1 in self._include_dirs: 328 | if url.find(dir1) != -1: 329 | return True 330 | 331 | return False 332 | -------------------------------------------------------------------------------- /mkdocs_add_number_plugin/utils.py: -------------------------------------------------------------------------------- 1 | 2 | def flatten(nav): 3 | """ 4 | Flattens mkdocs navigation to list of markdown files 5 | 6 | See tests/test_flatten.py for example 7 | 8 | Args: 9 | nav (list): nested list with dicts 10 | 11 | Returns: 12 | list: list of markdown pages 13 | """ 14 | pages = [] 15 | for i in nav: 16 | # file 17 | if type(i) == str: 18 | pages.append(i) 19 | continue 20 | item = list(i.values())[0] 21 | if type(item) == list: 22 | pages += flatten(item) 23 | else: 24 | pages.append(item) 25 | return pages 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='mkdocs-add-number-plugin', 8 | version='1.2.1', 9 | description='MkDocs Plugin to automatically number the headings (h1-h6) ' 10 | 'in each markdown page and number the nav.', 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | keywords='mkdocs index add-number plugin', 14 | url='https://github.com/shihr/mkdocs-add-number-plugin.git', 15 | author='ignorantshr', 16 | author_email='shrshraa@outlook.com', 17 | license='MIT', 18 | python_requires='>=3.5', 19 | install_requires=[ 20 | 'mkdocs>=1.1' 21 | ], 22 | classifiers=[ 23 | 'Intended Audience :: Developers', 24 | 'Intended Audience :: Information Technology', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 3 :: Only', 28 | 'Programming Language :: Python :: 3.5', 29 | 'Programming Language :: Python :: 3.6', 30 | 'Programming Language :: Python :: 3.7' 31 | ], 32 | packages=find_packages(), 33 | entry_points={ 34 | 'mkdocs.plugins': [ 35 | 'mkdocs-add-number-plugin=mkdocs_add_number_plugin.plugin:AddNumberPlugin', 36 | 'add-number=mkdocs_add_number_plugin.plugin:AddNumberPlugin' 37 | ] 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /tests/dummy_project/docs/a_third_page.md: -------------------------------------------------------------------------------- 1 | # A Third page 2 | 3 | But filename starts with an A. 4 | 5 | And this file does not have proper sequential headings 6 | 7 | ### heading three deep but no prior heading two deep 8 | 9 | From 1 to level 3. 10 | 11 | ## Some section 12 | 13 | back to heading one. 14 | 15 | ## sub heading 16 | 17 | ### another sub heading 18 | 19 | ## Another section 20 | 21 | -------------------------------------------------------------------------------- /tests/dummy_project/docs/first_page.md: -------------------------------------------------------------------------------- 1 | # First Test page 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 4 | 5 | some more content 6 | 7 | ## another heading 8 | 9 | more content 10 | 11 | ## Some section 12 | 13 | bla bla 14 | bla 15 | 16 | ### sub heading 17 | 18 | bla 19 | 20 | ### another sub heading 21 | 22 | ## Another section -------------------------------------------------------------------------------- /tests/dummy_project/docs/index.md: -------------------------------------------------------------------------------- 1 | # Test page 2 | 3 | Welcome to the test website 4 | 5 | some content 6 | 7 | ## another heading 8 | 9 | more content 10 | 11 | ## Some section 12 | 13 | bla bla 14 | bla 15 | 16 | ### sub heading three deep 17 | 18 | bla 19 | 20 | ### another sub heading 21 | 22 | ## Another section -------------------------------------------------------------------------------- /tests/dummy_project/docs/second_page.md: -------------------------------------------------------------------------------- 1 | # Second Test page 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 4 | 5 | some content 6 | 7 | ## another heading 8 | 9 | more content 10 | 11 | ## Some section 12 | 13 | bla bla 14 | bla 15 | 16 | ### sub heading 17 | 18 | bla 19 | 20 | ### another sub heading 21 | 22 | ## Another section -------------------------------------------------------------------------------- /tests/dummy_project/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: test plugin 2 | use_directory_urls: false 3 | 4 | plugins: 5 | - search 6 | - mkdocs-add-number-plugin: 7 | strict_mode: True -------------------------------------------------------------------------------- /tests/dummy_project/mkdocs_with_excludes.yml: -------------------------------------------------------------------------------- 1 | site_name: test plugin 2 | use_directory_urls: false 3 | 4 | plugins: 5 | - search 6 | - mkdocs-add-number-plugin: 7 | excludes: 8 | - second_page -------------------------------------------------------------------------------- /tests/dummy_project/mkdocs_with_nav.yml: -------------------------------------------------------------------------------- 1 | site_name: test plugin 2 | use_directory_urls: false 3 | 4 | nav: 5 | - Home: 'index.md' 6 | - Page One: 'first_page.md' 7 | - Page Two: 'second_page.md' 8 | - Page Three: 'a_third_page.md' 9 | 10 | plugins: 11 | - search 12 | - mkdocs-add-number-plugin -------------------------------------------------------------------------------- /tests/dummy_project/mkdocs_with_strict.yml: -------------------------------------------------------------------------------- 1 | site_name: test plugin 2 | use_directory_urls: false 3 | 4 | plugins: 5 | - search 6 | - mkdocs-add-number-plugin: 7 | strict_mode: True -------------------------------------------------------------------------------- /tests/test_builds.py: -------------------------------------------------------------------------------- 1 | """ 2 | Modules test that builds with different setting succeed. 3 | 4 | Note that pytest offers a `tmp_path`. 5 | You can reproduce locally with 6 | 7 | ```python 8 | %load_ext autoreload 9 | %autoreload 2 10 | import os 11 | import tempfile 12 | import shutil 13 | from pathlib import Path 14 | tmp_path = Path(tempfile.gettempdir()) / 'pytest-table-builder' 15 | if os.path.exists(tmp_path): 16 | shutil.rmtree(tmp_path) 17 | os.mkdir(tmp_path) 18 | ``` 19 | """ 20 | 21 | import re 22 | import os 23 | import shutil 24 | import logging 25 | from click.testing import CliRunner 26 | from mkdocs.__main__ import build_command 27 | 28 | def setup_clean_mkdocs_folder(mkdocs_yml_path, output_path): 29 | """ 30 | Sets up a clean mkdocs directory 31 | 32 | outputpath/testproject 33 | ├── docs/ 34 | └── mkdocs.yml 35 | 36 | Args: 37 | mkdocs_yml_path (Path): Path of mkdocs.yml file to use 38 | output_path (Path): Path of folder in which to create mkdocs project 39 | 40 | Returns: 41 | testproject_path (Path): Path to test project 42 | """ 43 | 44 | testproject_path = output_path / 'testproject' 45 | 46 | # Create empty 'testproject' folder 47 | if os.path.exists(testproject_path): 48 | logging.warning("""This command does not work on windows. 49 | Refactor your test to use setup_clean_mkdocs_folder() only once""") 50 | shutil.rmtree(testproject_path) 51 | 52 | # Copy correct mkdocs.yml file and our test 'docs/' 53 | shutil.copytree('tests/dummy_project/docs', testproject_path / 'docs') 54 | shutil.copyfile(mkdocs_yml_path, testproject_path / 'mkdocs.yml') 55 | 56 | return testproject_path 57 | 58 | 59 | def build_docs_setup(testproject_path): 60 | """ 61 | Runs the `mkdocs build` command 62 | 63 | Args: 64 | testproject_path (Path): Path to test project 65 | 66 | Returns: 67 | command: Object with results of command 68 | """ 69 | 70 | cwd = os.getcwd() 71 | os.chdir(testproject_path) 72 | 73 | try: 74 | run = CliRunner().invoke(build_command) 75 | os.chdir(cwd) 76 | return run 77 | except: 78 | os.chdir(cwd) 79 | raise 80 | 81 | def test_basic_build(tmp_path): 82 | 83 | tmp_proj = setup_clean_mkdocs_folder('tests/dummy_project/mkdocs.yml', tmp_path) 84 | result = build_docs_setup(tmp_proj) 85 | assert result.exit_code == 0, "'mkdocs build' command failed" 86 | 87 | index_file = tmp_proj / 'site/index.html' 88 | assert index_file.exists(), f"{index_file} does not exist" 89 | 90 | # index.html is always first page, so should have chapter 1 91 | contents = index_file.read_text() 92 | assert re.search(r"1. Test page", contents) 93 | assert re.search(r"1.2.1 sub heading three deep", contents) 94 | 95 | # 'a_third_page.md` is first alphabetical, but second due to index.md 96 | third_page = tmp_proj / 'site/a_third_page.html' 97 | contents = third_page.read_text() 98 | assert re.search(r"2. A Third page", contents) 99 | 100 | # 'second_page' is fourth in alphabetical order 101 | second_page = tmp_proj / 'site/second_page.html' 102 | contents = second_page.read_text() 103 | assert re.search(r"4. Second Test page", contents) 104 | 105 | def test_build_with_nav(tmp_path): 106 | 107 | tmp_proj = setup_clean_mkdocs_folder('tests/dummy_project/mkdocs_with_nav.yml', tmp_path) 108 | result = build_docs_setup(tmp_proj) 109 | assert result.exit_code == 0, "'mkdocs build' command failed" 110 | 111 | index_file = tmp_proj / 'site/index.html' 112 | assert index_file.exists(), f"{index_file} does not exist" 113 | 114 | # index.html is always first page, so should have chapter 1 115 | contents = index_file.read_text() 116 | assert re.search(r"1. Test page", contents) 117 | assert re.search(r"1.2.1 sub heading three deep", contents) 118 | 119 | # 'a_third_page.md` is .. you guessed it.. fourth :) 120 | third_page = tmp_proj / 'site/a_third_page.html' 121 | contents = third_page.read_text() 122 | assert re.search(r"4. A Third page", contents) 123 | 124 | # 'second_page' is 2+1 = third. 125 | second_page = tmp_proj / 'site/second_page.html' 126 | contents = second_page.read_text() 127 | assert re.search(r"3. Second Test page", contents) 128 | 129 | def test_build_with_excludes(tmp_path): 130 | """ 131 | currently not working, 132 | see https://github.com/ignorantshr/mkdocs-add-number-plugin/issues/8 133 | 134 | """ 135 | 136 | tmp_proj = setup_clean_mkdocs_folder('tests/dummy_project/mkdocs_with_excludes.yml', tmp_path) 137 | result = build_docs_setup(tmp_proj) 138 | assert result.exit_code == 0, "'mkdocs build' command failed" 139 | 140 | index_file = tmp_proj / 'site/index.html' 141 | assert index_file.exists(), f"{index_file} does not exist" 142 | 143 | # DISABLED for now. 144 | # second_page = tmp_proj / 'site/second_page.html' 145 | # assert not second_page.exists(), f"{second_page} should have been excluded" 146 | 147 | def test_build_with_strict(tmp_path): 148 | tmp_proj = setup_clean_mkdocs_folder('tests/dummy_project/mkdocs_with_strict.yml', tmp_path) 149 | result = build_docs_setup(tmp_proj) 150 | assert result.exit_code == 0, "'mkdocs build' command failed" 151 | 152 | index_file = tmp_proj / 'site/index.html' 153 | assert index_file.exists(), f"{index_file} does not exist" 154 | -------------------------------------------------------------------------------- /tests/test_flatten.py: -------------------------------------------------------------------------------- 1 | from mkdocs_add_number_plugin.utils import flatten 2 | 3 | 4 | def test_flatten(): 5 | 6 | nav = [ 7 | {"Home": "index.md"}, 8 | {"Page One": "page_one.md"}, 9 | {"Page Two": "page_two.md"}, 10 | {"Model Class": "model_class.md"}, 11 | { 12 | "TMD": [ 13 | {"Use Case": "TMD/1_use_case.md"}, 14 | {"Raw Data": "TMD/2_raw_data.md"}, 15 | {"Features": "TMD/3_features.md"}, 16 | {"Modelling": "TMD/4_modelling.md"}, 17 | {"Limitations": "TMD/5_limitations.md"}, 18 | ] 19 | }, 20 | { 21 | "Review Process": [ 22 | {"Early reflection": "review_process/early.md"}, 23 | {"Mature reflection": "review_process/mature.md"}, 24 | {"External reflection": "review_process/final.md"}, 25 | ] 26 | }, 27 | {"Approvals": "approvals.md"}, 28 | {"Appendix": "appendix.md"}, 29 | { 30 | "Cookbook": [ 31 | {"Markdown demos": "cookbook/markdown_demos.md"}, 32 | { 33 | "Correlation between features": "cookbook/correlation_between_features.md" 34 | }, 35 | ] 36 | }, 37 | ] 38 | 39 | assert flatten(nav) == [ 40 | "index.md", 41 | "page_one.md", 42 | "page_two.md", 43 | "model_class.md", 44 | "TMD/1_use_case.md", 45 | "TMD/2_raw_data.md", 46 | "TMD/3_features.md", 47 | "TMD/4_modelling.md", 48 | "TMD/5_limitations.md", 49 | "review_process/early.md", 50 | "review_process/mature.md", 51 | "review_process/final.md", 52 | "approvals.md", 53 | "appendix.md", 54 | "cookbook/markdown_demos.md", 55 | "cookbook/correlation_between_features.md", 56 | ] 57 | -------------------------------------------------------------------------------- /tests/test_markdown.py: -------------------------------------------------------------------------------- 1 | from mkdocs_add_number_plugin import markdown 2 | 3 | page1 = """ 4 | # Example page 5 | 6 | some content 7 | 8 | ## another heading 9 | 10 | more content 11 | 12 | ## Some section 13 | 14 | bla bla 15 | bla 16 | 17 | ### sub heading 18 | 19 | bla 20 | 21 | ### another sub heading 22 | 23 | ## Another section 24 | """ 25 | 26 | def test_headings(): 27 | lines = page1.split('\n') 28 | assert markdown.headings(lines) == { 29 | 1: '# Example page', 30 | 5: '## another heading', 31 | 9: '## Some section', 32 | 14: '### sub heading', 33 | 18: '### another sub heading', 34 | 20: '## Another section'} 35 | 36 | def test_heading_depth(): 37 | lines = page1.split('\n') 38 | heading_lines = markdown.headings(lines).values() 39 | assert [markdown.heading_depth(x) for x in heading_lines] == [ 40 | 1,2,2,3,3,2 41 | ] 42 | 43 | def test_update_heading_chapter(): 44 | line = "## 1.2.1 heading" 45 | assert markdown.update_heading_chapter(line, 4) == '## 4.2.1 heading' 46 | 47 | line = "#### 1.4.2.3 Another chapter!!1!" 48 | assert markdown.update_heading_chapter(line, 24) == '#### 24.4.2.3 Another chapter!!1!' -------------------------------------------------------------------------------- /tests/test_requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | mkdocs>=1.0.4 3 | pytest 4 | pytest-cov --------------------------------------------------------------------------------