├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ ├── pre-commit-autoupdate.yml │ ├── update-copyright-years.yml │ └── update-gh-actions.yml ├── .gitignore ├── .markdownlint-cli2.yaml ├── .pre-commit-config.yaml ├── .yamllint.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codecov.yml ├── examples ├── a-lot-of-includes │ ├── docs │ │ ├── barbaz.svg │ │ ├── included.md │ │ ├── includes │ │ │ ├── 00.md │ │ │ ├── 01.md │ │ │ ├── 02.md │ │ │ └── 03.md │ │ └── index.md │ └── mkdocs.yml ├── basic │ ├── docs │ │ ├── included.md │ │ └── index.md │ └── mkdocs.yml ├── custom-tags │ ├── docs │ │ ├── included.md │ │ └── index.md │ └── mkdocs.yml ├── frontmatters │ ├── docs │ │ ├── bad-multimarkdown.md │ │ ├── bad-yaml.md │ │ ├── good-multimarkdown.md │ │ ├── good-yaml.md │ │ └── index.md │ └── mkdocs.yml ├── global-exclude │ ├── docs │ │ ├── api │ │ │ ├── bar.md │ │ │ ├── baz │ │ │ │ └── qux.md │ │ │ └── foo.md │ │ ├── include.txt │ │ ├── index.md │ │ └── license.md │ └── mkdocs.yml ├── http-cache-dir │ ├── docs │ │ └── README.md │ └── mkdocs.yml ├── http-cache │ ├── docs │ │ └── README.md │ └── mkdocs.yml ├── include-if-exists │ ├── docs │ │ ├── empty.md │ │ ├── included.md │ │ └── index.md │ └── mkdocs.yml ├── include-to-list-item │ ├── docs │ │ └── index.md │ ├── included.md │ └── mkdocs.yml ├── long-line │ ├── docs │ │ └── index.md │ └── mkdocs.yml ├── start-end-not-found-warnings │ ├── docs │ │ ├── included.md │ │ └── index.md │ └── mkdocs.yml └── table-as-list-item │ ├── docs │ ├── index.md │ └── table.md │ └── mkdocs.yml ├── locale ├── es │ ├── README.md │ └── README.md.po └── fr │ ├── README.md │ └── README.md.po ├── lychee.toml ├── pyproject.toml ├── schema.json ├── setup.py ├── src └── mkdocs_include_markdown_plugin │ ├── __init__.py │ ├── cache.py │ ├── config.py │ ├── directive.py │ ├── event.py │ ├── files_watcher.py │ ├── logger.py │ ├── plugin.py │ ├── process.py │ └── py.typed └── tests ├── __init__.py ├── conftest.py ├── test_integration ├── test_cache_integration.py └── test_examples.py ├── test_unit ├── test_arguments.py ├── test_cache.py ├── test_config.py ├── test_encoding.py ├── test_exclude.py ├── test_glob_include.py ├── test_include.py ├── test_include_markdown.py ├── test_logging.py ├── test_nested_includes.py └── test_process.py └── testing_helpers.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | 9 | [*.{py}] 10 | indent_size = 4 11 | 12 | [*.{py,md}] 13 | trim_trailing_whitespace = false 14 | 15 | [*.toml] 16 | indent_size = 2 17 | 18 | [*.json] 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | examples/a-lot-of-includes/docs/index.md -diff 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.10" 22 | - name: Install dependencies 23 | run: pip install -U hatch 24 | - name: Lint 25 | run: hatch run style:lint 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | lychee: 30 | name: Check URLs 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Restore lychee cache 35 | uses: actions/cache@v4 36 | with: 37 | path: .lycheecache 38 | key: cache-lychee-${{ github.sha }} 39 | restore-keys: cache-lychee- 40 | - name: Run Lychee 41 | uses: lycheeverse/lychee-action@v2 42 | with: 43 | args: --cache --max-cache-age 1d . 44 | 45 | test: 46 | name: Test 47 | runs-on: ${{ matrix.platform }} 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | py: 52 | - 3.9 53 | - "3.10" 54 | - "3.11" 55 | - "3.12" 56 | - "3.13" 57 | platform: 58 | - ubuntu-latest 59 | - macos-latest 60 | - windows-latest 61 | steps: 62 | - uses: actions/checkout@v4 63 | - name: Set up Python v${{ matrix.py }} 64 | uses: actions/setup-python@v5 65 | with: 66 | python-version: ${{ matrix.py }} 67 | - name: Pick environment to run 68 | id: env 69 | shell: python 70 | run: | 71 | import codecs, os, sys 72 | env = f"py=py3{sys.version_info[1]}\n" 73 | sys.stdout.write(f"Picked {env.split('=')[1].strip()} for {sys.version}\n") 74 | with codecs.open(os.environ["GITHUB_OUTPUT"], "a", "utf-8") as file_handler: 75 | file_handler.write(env) 76 | - name: Install dependencies 77 | run: pip install -U hatch 78 | - name: Run tests 79 | run: | 80 | hatch run +py=${{ steps.env.outputs.py }} tests:all 81 | - name: Convert coverage to XML 82 | run: | 83 | pip install coverage covdefaults 84 | coverage combine 85 | coverage xml 86 | - name: Upload coverage 87 | uses: codecov/codecov-action@v4 88 | env: 89 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 90 | 91 | build-dist: 92 | if: startsWith(github.ref, 'refs/tags/') 93 | needs: 94 | - lint 95 | - test 96 | name: Build package 97 | runs-on: ubuntu-latest 98 | steps: 99 | - uses: actions/checkout@v4 100 | - uses: actions/setup-python@v5 101 | name: Install Python 102 | with: 103 | python-version: "3.11" 104 | - name: Install build dependencies 105 | run: pip install -U hatch 106 | - name: Build package 107 | run: hatch build 108 | - uses: actions/upload-artifact@v4 109 | with: 110 | path: dist/* 111 | name: distribution 112 | 113 | pypi-upload: 114 | name: Upload to PyPI 115 | needs: build-dist 116 | runs-on: ubuntu-latest 117 | steps: 118 | - uses: actions/download-artifact@v4 119 | with: 120 | name: distribution 121 | path: dist 122 | - uses: pypa/gh-action-pypi-publish@v1.12.4 123 | with: 124 | user: __token__ 125 | password: ${{ secrets.PYPI_TOKEN }} 126 | # repository_url: https://test.pypi.org/legacy/ 127 | skip-existing: true 128 | 129 | release: 130 | name: Release 131 | needs: build-dist 132 | runs-on: ubuntu-latest 133 | steps: 134 | - uses: actions/checkout@v4 135 | - name: Get tag metadata 136 | id: tag 137 | run: | 138 | TAG_TITLE=${GITHUB_REF#refs/*/} 139 | echo "title=$TAG_TITLE" >> $GITHUB_OUTPUT 140 | git -c protocol.version=2 fetch --prune --progress \ 141 | --no-recurse-submodules origin \ 142 | +refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/* 143 | TAG_BODY="$(git tag -l --format='%(contents)' $TAG_TITLE)" 144 | TAG_BODY="${TAG_BODY//'%'/'%25'}" 145 | TAG_BODY="${TAG_BODY//$'\n'/'%0A'}" 146 | TAG_BODY="${TAG_BODY//$'\r'/'%0D'}" 147 | echo "body=$TAG_BODY" >> $GITHUB_OUTPUT 148 | - name: Create Release 149 | uses: softprops/action-gh-release@v2 150 | id: create-release 151 | with: 152 | name: ${{ steps.tag.outputs.title }} 153 | tag_name: ${{ steps.tag.outputs.title }} 154 | body: ${{ steps.tag.outputs.body }} 155 | draft: false 156 | prerelease: false 157 | - uses: actions/download-artifact@v4 158 | name: Download builds 159 | with: 160 | name: distribution 161 | path: dist 162 | - uses: shogo82148/actions-upload-release-asset@v1 163 | name: Upload release assets 164 | with: 165 | upload_url: ${{ steps.create-release.outputs.upload_url }} 166 | asset_path: dist/* 167 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-autoupdate.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit autoupdate 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | schedule: 9 | - cron: 0 3 1 1/6 * 10 | workflow_dispatch: 11 | 12 | jobs: 13 | autoupdate: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.12" 21 | - name: Install pre-commit 22 | run: pip install pre-commit 23 | - name: Run pre-commit autoupdate 24 | run: pre-commit autoupdate 25 | - name: Open pull request 26 | uses: peter-evans/create-pull-request@v6.1.0 27 | with: 28 | branch: pre-commit-autoupdate 29 | title: Upgrade pre-commit hooks revisions 30 | commit-message: Upgrade pre-commit hooks revisions 31 | body: "Upgrades revisions of pre-commit hooks tools to latest versions :hammer_and_wrench:" 32 | labels: dependencies 33 | delete-branch: true 34 | -------------------------------------------------------------------------------- /.github/workflows/update-copyright-years.yml: -------------------------------------------------------------------------------- 1 | name: Update copyright years in license file 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | schedule: 9 | - cron: 0 3 2 1 * 10 | workflow_dispatch: 11 | 12 | jobs: 13 | action-update-license-year: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - uses: FantasticFiasco/action-update-license-year@v3 20 | with: 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/update-gh-actions.yml: -------------------------------------------------------------------------------- 1 | name: Update GitHub Actions versions 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: 0 3 1 1/6 * 7 | 8 | jobs: 9 | update: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | token: ${{ secrets.UPDATE_GH_ACTIONS_TOKEN }} 15 | 16 | - name: Run GitHub Actions Version Updater 17 | uses: saadmk11/github-actions-version-updater@v0.8.1 18 | with: 19 | token: ${{ secrets.UPDATE_GH_ACTIONS_TOKEN }} 20 | release_types: major 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | __pycache__/ 4 | .hatch/ 5 | report.html 6 | .coverage 7 | .pytest_cache/ 8 | .ruff_cache/ 9 | htmlcov/ 10 | *.egg-info/ 11 | venv*/ 12 | *.whl 13 | *.tgz 14 | *.mo 15 | *.pot 16 | .vscode 17 | .mypy_cache/ 18 | *.so 19 | .prettier-cache 20 | /*.html 21 | 22 | site/ 23 | -------------------------------------------------------------------------------- /.markdownlint-cli2.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | no-duplicate-heading: false 3 | no-inline-html: false 4 | emphasis-style: 5 | style: underscore 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/rbubley/mirrors-prettier 3 | rev: v3.4.2 4 | hooks: 5 | - id: prettier 6 | types_or: 7 | - toml 8 | - css 9 | - json 10 | - json5 11 | - yaml 12 | - markdown 13 | additional_dependencies: 14 | - prettier 15 | - prettier-plugin-toml 16 | minimum_pre_commit_version: 2.9.0 17 | exclude: ^locale 18 | args: 19 | - --cache 20 | - --cache-location=.prettier-cache 21 | - --ignore-path=.gitignore 22 | - repo: https://github.com/mondeja/project-config 23 | rev: v0.9.7 24 | hooks: 25 | - id: project-config 26 | - repo: meta 27 | hooks: 28 | - id: check-hooks-apply 29 | name: check-hooks-apply 30 | - id: check-useless-excludes 31 | name: check-useless-excludes 32 | - repo: https://github.com/pre-commit/pre-commit-hooks 33 | rev: v5.0.0 34 | hooks: 35 | - id: trailing-whitespace 36 | name: trailing-whitespace 37 | - id: end-of-file-fixer 38 | name: end-of-file-fixer 39 | - repo: https://github.com/python-jsonschema/check-jsonschema 40 | rev: 0.30.0 41 | hooks: 42 | - id: check-github-workflows 43 | name: check-github-workflows 44 | args: 45 | - --verbose 46 | - repo: https://github.com/astral-sh/ruff-pre-commit 47 | rev: v0.8.4 48 | hooks: 49 | - id: ruff 50 | args: 51 | - --fix 52 | - --exit-non-zero-on-fix 53 | - repo: https://github.com/pre-commit/mirrors-autopep8 54 | rev: v2.0.4 55 | hooks: 56 | - id: autopep8 57 | - repo: https://github.com/mondeja/mdpo 58 | rev: v2.1.3 59 | hooks: 60 | - id: md2po2md 61 | files: ^README\.md 62 | args: 63 | - --no-location 64 | - --no-obsolete 65 | - --no-empty-msgstr 66 | - -l 67 | - es 68 | - -l 69 | - fr 70 | - -o 71 | - locale/{lang} 72 | - repo: https://github.com/mondeja/pre-commit-po-hooks 73 | rev: v1.7.3 74 | hooks: 75 | - id: remove-metadata 76 | - repo: https://github.com/adrienverge/yamllint 77 | rev: v1.35.1 78 | hooks: 79 | - id: yamllint 80 | - repo: https://github.com/DavidAnson/markdownlint-cli2 81 | rev: v0.17.1 82 | hooks: 83 | - id: markdownlint-cli2 84 | name: markdownlint-readme 85 | files: ^README.md 86 | - repo: https://github.com/editorconfig-checker/editorconfig-checker.python 87 | rev: 3.0.3 88 | hooks: 89 | - id: editorconfig-checker 90 | name: editorconfig-checker 91 | alias: ec 92 | - repo: https://github.com/pre-commit/mirrors-mypy 93 | rev: v1.14.1 94 | hooks: 95 | - id: mypy 96 | name: mypy-mkdocs-1.5.0 97 | files: ^src 98 | additional_dependencies: 99 | - mkdocs>=1.5.0,<1.6.0 100 | - platformdirs 101 | - wcmatch 102 | - repo: https://github.com/pre-commit/mirrors-mypy 103 | rev: v1.14.1 104 | hooks: 105 | - id: mypy 106 | name: mypy-mkdocs-1.6.0 107 | files: ^src 108 | additional_dependencies: 109 | - mkdocs>=1.6.0,<1.7.0 110 | - platformdirs 111 | - wcmatch 112 | -------------------------------------------------------------------------------- /.yamllint.yaml: -------------------------------------------------------------------------------- 1 | ignore: | 2 | venv 3 | build 4 | 5 | rules: 6 | braces: 7 | forbid: true 8 | brackets: 9 | forbid: true 10 | colons: 11 | max-spaces-after: 1 12 | max-spaces-before: 0 13 | comments: 14 | ignore-shebangs: true 15 | min-spaces-from-content: 2 16 | require-starting-space: true 17 | comments-indentation: enable 18 | empty-lines: 19 | max: 1 20 | hyphens: 21 | max-spaces-after: 1 22 | indentation: 23 | check-multi-line-strings: true 24 | indent-sequences: true 25 | spaces: consistent 26 | ignore: | 27 | .github 28 | key-duplicates: enable 29 | key-ordering: disable 30 | line-length: 31 | allow-non-breakable-words: true 32 | max: 180 33 | ignore: | 34 | .github 35 | new-lines: 36 | type: unix 37 | octal-values: 38 | forbid-explicit-octal: true 39 | forbid-implicit-octal: true 40 | quoted-strings: 41 | extra-required: 42 | # Dependabot does not parse unquoted hours as strings: 43 | # https://github.community/t/dependabot-parser-treat-hours-as-integers/145596 44 | - \d\d:\d\d 45 | quote-type: double 46 | required: only-when-needed 47 | trailing-spaces: enable 48 | truthy: disable 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | 3 | ## Development install 4 | 5 | ```sh 6 | git clone https://github.com/mondeja/mkdocs-include-markdown-plugin 7 | cd mkdocs-include-markdown-plugin 8 | pip install hatch 9 | ``` 10 | 11 | ## Test 12 | 13 | ```sh 14 | hatch run tests:unit 15 | # `hatch run tests:integration` 16 | # `hatch run tests:all` 17 | # `hatch run tests:cov` 18 | ``` 19 | 20 | ## Linting and translations processing 21 | 22 | ```sh 23 | hatch run style:lint 24 | ``` 25 | 26 | ## Release 27 | 28 | ```sh 29 | version="$(hatch run bump )" 30 | git add . 31 | git commit -m "Bump version" 32 | git push origin master 33 | git tag -a "v$version" 34 | git push origin "v$version" 35 | ``` 36 | 37 | ## Compatibility 38 | 39 | Latest version supporting Python3.7 and Mkdocs<1.4.0 is v4.0.4. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2017-2025 Joe Rickerby and contributors 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # mkdocs-include-markdown-plugin 4 | 5 | 6 | 7 | [![PyPI][pypi-version-badge-link]][pypi-link] 8 | [![License][license-image]][license-link] 9 | [![Tests][tests-image]][tests-link] 10 | [![Coverage status][coverage-image]][coverage-link] 11 | [![Downloads][downloads-image]][downloads-link] 12 | 13 | 14 | 15 | Mkdocs Markdown includer plugin. 16 | 17 | 18 | 19 | 20 | > Read this document in other languages: 21 | > 22 | > - [Español][es-readme-link] 23 | > - [Français][fr-readme-link] 24 | 25 | 26 | 27 | ## Installation 28 | 29 | ```bash 30 | pip install mkdocs-include-markdown-plugin 31 | ``` 32 | 33 | ## Documentation 34 | 35 | ### Setup 36 | 37 | Enable the plugin in your `mkdocs.yml`: 38 | 39 | ```yaml 40 | plugins: 41 | - include-markdown 42 | ``` 43 | 44 | ### Configuration 45 | 46 | The global behaviour of the plugin can be customized in the configuration. 47 | 48 | Most of the settings will define the default values passed to arguments 49 | of directives and are documented in the [reference](#reference). 50 | 51 | ```yaml 52 | plugins: 53 | - include-markdown: 54 | encoding: ascii 55 | preserve_includer_indent: false 56 | dedent: false 57 | trailing_newlines: true 58 | comments: true 59 | rewrite_relative_urls: true 60 | heading_offset: 0 61 | start: 62 | end: 63 | recursive: true 64 | ``` 65 | 66 | #### `opening_tag` and `closing_tag` 67 | 68 | Default opening and closing tags. When not specified they are `{%` and `%}`. 69 | 70 | ```yaml 71 | plugins: 72 | - include-markdown: 73 | opening_tag: "{!" 74 | closing_tag: "!}" 75 | ``` 76 | 77 | 78 | 79 | #### `exclude` 80 | 81 | Global exclusion wildcard patterns. Relative paths defined here will be 82 | relative to the [`docs_dir`] directory. 83 | 84 | ```yaml 85 | plugins: 86 | - include-markdown: 87 | exclude: 88 | - LICENSE.md 89 | - api/** 90 | ``` 91 | 92 | 93 | 94 | #### `cache` 95 | 96 | Expiration time in seconds for cached HTTP requests when including from URLs. 97 | 98 | ```yaml 99 | plugins: 100 | - include-markdown: 101 | cache: 600 102 | ``` 103 | 104 | In order to use this feature, the dependency [platformdirs] must be installed 105 | or the setting [`cache_dir`](#cache_dir) must be defined. You can include 106 | [platformdirs] in the installation of the plugin adding the `cache` extra: 107 | 108 | ```txt 109 | # requirements.txt 110 | mkdocs-include-markdown-plugin[cache] 111 | ``` 112 | 113 | 114 | 115 | #### `cache_dir` 116 | 117 | Directory where cached HTTP requests will be stored. If set, [platformdirs] is not 118 | needed to be installed to use [`cache`](#cache). 119 | 120 | ```yaml 121 | plugins: 122 | - include-markdown: 123 | cache: 600 124 | cache_dir: ./mkdocs-include-markdown-cache 125 | ``` 126 | 127 | A _.gitignore_ file will be added to the cache directory if not exists to avoid 128 | committing the cache files. 129 | 130 | 131 | 132 | #### `directives` 133 | 134 | Customize the names of the directives. 135 | 136 | ```yaml 137 | plugins: 138 | - include-markdown: 139 | directives: 140 | include-markdown: include-md 141 | include: replace 142 | ``` 143 | 144 | ### Reference 145 | 146 | This plugin provides two directives, one to include Markdown files and another 147 | to include files of any type. 148 | 149 | Paths of included files can be either: 150 | 151 | - URLs to include remote content. 152 | - Local files: 153 | - Absolute paths (starting with a path separator). 154 | - Relative from the file that includes them (starting with `./` or `../`). 155 | - Relative to the [`docs_dir`] directory. For instance if your `docs_dir` is 156 | _./docs/_, then `includes/header.md` will match the file 157 | _./docs/includes/header.md_. 158 | - [Bash wildcard globs] matching multiple local files. 159 | 160 | File paths to include and string arguments can be wrapped by double `"` or 161 | single `'` quotes, which can be escaped prepending them a `\` character as 162 | `\"` and `\'`. 163 | 164 | The arguments **start** and **end** may contain usual (Python-style) escape 165 | sequences like `\n` to match against newlines. 166 | 167 | 168 | 169 | #### **`include-markdown`** 170 | 171 | Includes Markdown files content, optionally using two delimiters to filter the 172 | content to include. 173 | 174 | - # 175 | **start**: Delimiter that marks the beginning of the content to include. 176 | - # 177 | **end**: Delimiter that marks the end of the content to include. 178 | - # 179 | **preserve-includer-indent** (_true_): When this option is enabled (default), 180 | every line of the content to include is indented with the same number of 181 | spaces used to indent the includer `{% %}` template. Possible values are 182 | `true` and `false`. 183 | - # 184 | **dedent** (_false_): If enabled, the included content will be dedented. 185 | - # 186 | **exclude**: Specify with a glob which files should be ignored. Only useful 187 | when passing globs to include multiple files. 188 | - # 189 | **trailing-newlines** (_true_): When this option is disabled, the trailing newlines 190 | found in the content to include are stripped. Possible values are `true` and `false`. 191 | - # 192 | **recursive** (_true_): When this option is disabled, included files are not 193 | processed for recursive includes. Possible values are `true` and `false`. 194 | - # 195 | **encoding** (_'utf-8'_): Specify the encoding of the included file. 196 | If not defined `'utf-8'` will be used. 197 | - # 198 | **rewrite-relative-urls** (_true_): When this option is enabled (default), 199 | Markdown links and images in the content that are specified by a relative URL 200 | are rewritten to work correctly in their new location. Possible values are 201 | `true` and `false`. 202 | - # 203 | **comments** (_false_): When this option is enabled, the content to include 204 | is wrapped by `` and `` comments 205 | which help to identify that the content has been included. Possible values 206 | are `true` and `false`. 207 | - # 208 | **heading-offset** (0): Increases or decreases the Markdown headings depth 209 | by this number. Only supports number sign (`#`) heading syntax. Accepts 210 | negative values to drop leading `#` characters. 211 | 212 | ##### Examples 213 | 214 | ```jinja 215 | {% 216 | include-markdown "../README.md" 217 | start="" 218 | end="" 219 | %} 220 | ``` 221 | 222 | ```jinja 223 | {% 224 | include-markdown 'includes/header.md' 225 | start='' 226 | end='' 227 | rewrite-relative-urls=false 228 | comments=true 229 | %} 230 | ``` 231 | 232 | ```jinja 233 | {% 234 | include-markdown "includes/header.md" 235 | heading-offset=1 236 | %} 237 | ``` 238 | 239 | ```jinja 240 | {% 241 | include-markdown "../LICENSE*" 242 | start="" 243 | end='' 244 | exclude="../*.rst" 245 | %} 246 | ``` 247 | 248 | ```jinja 249 | {% 250 | include-markdown "**" 251 | exclude="./{index,LICENSE}.md" 252 | %} 253 | ``` 254 | 255 | ```jinja 256 | {% include-markdown '/escap\'ed/single-quotes/in/file\'/name.md' %} 257 | ``` 258 | 259 | 260 | 261 | #### **`include`** 262 | 263 | Includes the content of a file or a group of files. 264 | 265 | - # 266 | **start**: Delimiter that marks the beginning of the content to include. 267 | - # 268 | **end**: Delimiter that marks the end of the content to include. 269 | - # 270 | **preserve-includer-indent** (_true_): When this option is enabled (default), 271 | every line of the content to include is indented with the same number of 272 | spaces used to indent the includer `{% %}` template. Possible values are 273 | `true` and `false`. 274 | - # 275 | **dedent** (_false_): If enabled, the included content will be dedented. 276 | - # 277 | **exclude**: Specify with a glob which files should be ignored. Only useful 278 | when passing globs to include multiple files. 279 | - # 280 | **trailing-newlines** (_true_): When this option is disabled, the trailing newlines 281 | found in the content to include are stripped. Possible values are `true` and `false`. 282 | - # 283 | **recursive** (_true_): When this option is disabled, included files are not 284 | processed for recursive includes. Possible values are `true` and `false`. 285 | - # 286 | **encoding** (_'utf-8'_): Specify the encoding of the included file. 287 | If not defined `'utf-8'` will be used. 288 | 289 | ##### Examples 290 | 291 | ```jinja 292 | ~~~yaml 293 | {% include "../examples/github-minimal.yml" %} 294 | ~~~ 295 | ``` 296 | 297 | ```jinja 298 | {% 299 | include "../examples.md" 300 | start="~~~yaml" 301 | end="~~~\n" 302 | %} 303 | ``` 304 | 305 | ```jinja 306 | {% 307 | include '**' 308 | exclude='./*.md' 309 | %} 310 | ``` 311 | 312 | ## Acknowledgment 313 | 314 | - [Joe Rickerby] and [contributors] for 315 | [giving me the permissions][cibuildwheel-470] to 316 | [separate this plugin][cibuildwheel-475] from the 317 | documentation of [cibuildwheel][cibuildwheel-repo-link]. 318 | 319 | [Bash wildcard globs]: https://facelessuser.github.io/wcmatch/glob/#syntax 320 | 321 | 322 | 323 | [pypi-link]: https://pypi.org/project/mkdocs-include-markdown-plugin 324 | [pypi-version-badge-link]: https://img.shields.io/pypi/v/mkdocs-include-markdown-plugin?logo=pypi&logoColor=white 325 | [tests-image]: https://img.shields.io/github/actions/workflow/status/mondeja/mkdocs-include-markdown-plugin/ci.yml?logo=github&label=tests&branch=master 326 | [tests-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/actions?query=workflow%3ACI 327 | [coverage-image]: https://img.shields.io/codecov/c/github/mondeja/mkdocs-include-markdown-plugin?logo=codecov&logoColor=white 328 | [coverage-link]: https://app.codecov.io/gh/mondeja/mkdocs-include-markdown-plugin 329 | [license-image]: https://img.shields.io/pypi/l/mkdocs-include-markdown-plugin?color=light-green&logo=apache&logoColor=white 330 | [license-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/LICENSE 331 | [downloads-image]: https://img.shields.io/pypi/dm/mkdocs-include-markdown-plugin 332 | [downloads-link]: https://pepy.tech/project/mkdocs-include-markdown-plugin 333 | [platformdirs]: https://pypi.org/project/platformdirs/ 334 | [cibuildwheel-470]: https://github.com/pypa/cibuildwheel/issues/470 335 | [cibuildwheel-475]: https://github.com/pypa/cibuildwheel/pull/475 336 | [cibuildwheel-repo-link]: https://github.com/pypa/cibuildwheel 337 | [es-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/es/README.md 338 | [fr-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/fr/README.md 339 | [`docs_dir`]: https://www.mkdocs.org/user-guide/configuration/#docs_dir 340 | [Joe Rickerby]: https://github.com/joerick 341 | [contributors]: https://github.com/mondeja/mkdocs-include-markdown-plugin/graphs/contributors 342 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | informational: true 7 | target: 100% 8 | threshold: 2% 9 | -------------------------------------------------------------------------------- /examples/a-lot-of-includes/docs/barbaz.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/aa17cc062d5a63caec5f43c0425974c6a34725be/examples/a-lot-of-includes/docs/barbaz.svg -------------------------------------------------------------------------------- /examples/a-lot-of-includes/docs/included.md: -------------------------------------------------------------------------------- 1 | Some ignored content. 2 | 3 | 4 | 5 | Some included content. 6 | 7 | ![Image](barbaz.svg) 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/a-lot-of-includes/docs/includes/00.md: -------------------------------------------------------------------------------- 1 | Foo bar baz 00 2 | Foo bar baz 00 3 | Foo bar baz 00 4 | -------------------------------------------------------------------------------- /examples/a-lot-of-includes/docs/includes/01.md: -------------------------------------------------------------------------------- 1 | Foo bar baz 01 2 | Foo bar baz 01 3 | Foo bar baz 01 4 | -------------------------------------------------------------------------------- /examples/a-lot-of-includes/docs/includes/02.md: -------------------------------------------------------------------------------- 1 | Foo bar baz 02 2 | Foo bar baz 02 3 | Foo bar baz 02 4 | -------------------------------------------------------------------------------- /examples/a-lot-of-includes/docs/includes/03.md: -------------------------------------------------------------------------------- 1 | Foo bar baz 03 2 | Foo bar baz 03 3 | Foo bar baz 03 4 | -------------------------------------------------------------------------------- /examples/a-lot-of-includes/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Foo 2 | plugins: 3 | - include-markdown 4 | -------------------------------------------------------------------------------- /examples/basic/docs/included.md: -------------------------------------------------------------------------------- 1 | Some ignored content. 2 | 3 | <--start--> 4 | 5 | Some included content. 6 | -------------------------------------------------------------------------------- /examples/basic/docs/index.md: -------------------------------------------------------------------------------- 1 | {% include-markdown './included.md' start='<--start-->' %} 2 | -------------------------------------------------------------------------------- /examples/basic/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Foo 2 | plugins: 3 | - include-markdown 4 | -------------------------------------------------------------------------------- /examples/custom-tags/docs/included.md: -------------------------------------------------------------------------------- 1 | Content to include. 2 | -------------------------------------------------------------------------------- /examples/custom-tags/docs/index.md: -------------------------------------------------------------------------------- 1 | # Home 2 | 3 | |> 4 | include-markdown "./included.md" 5 | comments=true 6 | <| 7 | 8 | |> include './included.md' <| 9 | -------------------------------------------------------------------------------- /examples/custom-tags/mkdocs.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - include-markdown: 3 | opening_tag: "|>" 4 | closing_tag: <| 5 | site_name: Foo 6 | -------------------------------------------------------------------------------- /examples/frontmatters/docs/bad-multimarkdown.md: -------------------------------------------------------------------------------- 1 | Foo: bar 2 | Some content to include from _bad-multimarkdown.md_. 3 | -------------------------------------------------------------------------------- /examples/frontmatters/docs/bad-yaml.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar 3 | --- 4 | 5 | Some content to include from _bad-yaml.md_. 6 | -------------------------------------------------------------------------------- /examples/frontmatters/docs/good-multimarkdown.md: -------------------------------------------------------------------------------- 1 | Foo: bar 2 | 3 | Some content to include from _good-multimarkdown.md_. 4 | -------------------------------------------------------------------------------- /examples/frontmatters/docs/good-yaml.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar 3 | --- 4 | 5 | Some content to include from _good-yaml.md_. 6 | -------------------------------------------------------------------------------- /examples/frontmatters/docs/index.md: -------------------------------------------------------------------------------- 1 | ### Inclusion from file with YAML frontmatter 2 | 3 | {% include-markdown './good-yaml.md' start='---\n\n' %} 4 | 5 | {% include-markdown './bad-yaml.md' start='---\n\n' %} 6 | 7 | ### Inclusion from file with MultiMarkdown frontmatter 8 | 9 | {% include-markdown './good-multimarkdown.md' start='\n\n' %} 10 | 11 | {% include-markdown './bad-multimarkdown.md' start='\n\n' %} 12 | -------------------------------------------------------------------------------- /examples/frontmatters/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: My Docs 2 | plugins: 3 | - include-markdown 4 | -------------------------------------------------------------------------------- /examples/global-exclude/docs/api/bar.md: -------------------------------------------------------------------------------- 1 | This should not be included. 2 | -------------------------------------------------------------------------------- /examples/global-exclude/docs/api/baz/qux.md: -------------------------------------------------------------------------------- 1 | This should not be included. 2 | -------------------------------------------------------------------------------- /examples/global-exclude/docs/api/foo.md: -------------------------------------------------------------------------------- 1 | This should not be included. 2 | -------------------------------------------------------------------------------- /examples/global-exclude/docs/include.txt: -------------------------------------------------------------------------------- 1 | This should be included. 2 | -------------------------------------------------------------------------------- /examples/global-exclude/docs/index.md: -------------------------------------------------------------------------------- 1 | {% include-markdown "**" exclude="./{index,license}.md" %} 2 | {% include "**" exclude="./{index,license}.md" %} 3 | -------------------------------------------------------------------------------- /examples/global-exclude/docs/license.md: -------------------------------------------------------------------------------- 1 | This should not be included. 2 | -------------------------------------------------------------------------------- /examples/global-exclude/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: My Docs 2 | plugins: 3 | - include-markdown: 4 | exclude: 5 | - api/** 6 | -------------------------------------------------------------------------------- /examples/http-cache-dir/docs/README.md: -------------------------------------------------------------------------------- 1 | # Header 2 | 3 | {% 4 | include-markdown "https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/master/examples/basic/docs/included.md" 5 | start="<--start-->" 6 | %} 7 | 8 | ## mkdocs.yml 9 | 10 | ``` 11 | {% 12 | include "https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/master/examples/basic/mkdocs.yml" 13 | %} 14 | ``` 15 | 16 | ## From cache 17 | 18 | {% 19 | include-markdown "https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/master/examples/basic/docs/included.md" 20 | start="<--start-->" 21 | %} 22 | -------------------------------------------------------------------------------- /examples/http-cache-dir/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Foo 2 | plugins: 3 | - include-markdown: 4 | cache: 600 5 | cache_dir: .mkdocs-include-markdown-cache 6 | -------------------------------------------------------------------------------- /examples/http-cache/docs/README.md: -------------------------------------------------------------------------------- 1 | # Header 2 | 3 | :warning: You need to install `platformdirs` to run this example. 4 | 5 | {% 6 | include-markdown "https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/master/examples/basic/docs/included.md" 7 | start="<--start-->" 8 | %} 9 | 10 | ## mkdocs.yml 11 | 12 | ``` 13 | {% 14 | include "https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/master/examples/basic/mkdocs.yml" 15 | %} 16 | ``` 17 | 18 | ## From cache 19 | 20 | {% 21 | include-markdown "https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/master/examples/basic/docs/included.md" 22 | start="<--start-->" 23 | %} 24 | -------------------------------------------------------------------------------- /examples/http-cache/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Foo 2 | plugins: 3 | - include-markdown: 4 | cache: 600 5 | -------------------------------------------------------------------------------- /examples/include-if-exists/docs/empty.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/aa17cc062d5a63caec5f43c0425974c6a34725be/examples/include-if-exists/docs/empty.md -------------------------------------------------------------------------------- /examples/include-if-exists/docs/included.md: -------------------------------------------------------------------------------- 1 | Some included content. 2 | -------------------------------------------------------------------------------- /examples/include-if-exists/docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include-markdown '?(empty.md)?(included.md)' %} 4 | 5 | {% include-markdown '?(empty.md)?(not-existent.md)' %} 6 | -------------------------------------------------------------------------------- /examples/include-if-exists/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Foo 2 | plugins: 3 | - include-markdown 4 | -------------------------------------------------------------------------------- /examples/include-to-list-item/docs/index.md: -------------------------------------------------------------------------------- 1 | 1. This is the first number line 2 | 3 | 1. {% include-markdown "../included.md" %} 4 | 5 | 1. If everything works as expected this should be number 3 6 | -------------------------------------------------------------------------------- /examples/include-to-list-item/included.md: -------------------------------------------------------------------------------- 1 | This content chunk contains code 2 | 3 | ``` 4 | This is my example 5 | It is a code block 6 | ``` 7 | 8 | With some text after it 9 | -------------------------------------------------------------------------------- /examples/include-to-list-item/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Foo 2 | plugins: 3 | - include-markdown 4 | -------------------------------------------------------------------------------- /examples/long-line/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Foo 2 | plugins: 3 | - include-markdown 4 | -------------------------------------------------------------------------------- /examples/start-end-not-found-warnings/docs/included.md: -------------------------------------------------------------------------------- 1 | Delimiters not found, so this inclusion must raise warnings. 2 | -------------------------------------------------------------------------------- /examples/start-end-not-found-warnings/docs/index.md: -------------------------------------------------------------------------------- 1 | # Home 2 | 3 | {% 4 | include-markdown "./included.md" 5 | start="<--start-->" 6 | end="<--end-->" 7 | %} 8 | 9 | {% 10 | include './included.md' 11 | start="<--start-->" 12 | end="<--end-->" 13 | comments=true 14 | %} 15 | -------------------------------------------------------------------------------- /examples/start-end-not-found-warnings/mkdocs.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - include-markdown 3 | site_name: Foo 4 | -------------------------------------------------------------------------------- /examples/table-as-list-item/docs/index.md: -------------------------------------------------------------------------------- 1 | # Table included as part of a list item 2 | 3 | 1. A list entry 4 | 1. Item with table 5 | 6 | {% 7 | include-markdown "./table.md" 8 | %} 9 | 10 | 1. Another list entry 11 | 12 | ## Expected output 13 | 14 | 1. A list entry 15 | 1. Item with table 16 | 17 | | A | B | 18 | | --- | --- | 19 | | foo | bar | 20 | 21 | 1. Another list entry 22 | -------------------------------------------------------------------------------- /examples/table-as-list-item/docs/table.md: -------------------------------------------------------------------------------- 1 | | A | B | 2 | | --- | --- | 3 | | foo | bar | 4 | -------------------------------------------------------------------------------- /examples/table-as-list-item/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Foo 2 | plugins: 3 | - include-markdown 4 | -------------------------------------------------------------------------------- /locale/es/README.md: -------------------------------------------------------------------------------- 1 | # mkdocs-include-markdown-plugin 2 | 3 | [![PyPI][pypi-version-badge-link]][pypi-link] 4 | [![License][license-image]][license-link] [![Tests][tests-image]][tests-link] 5 | [![Coverage status][coverage-image]][coverage-link] 6 | [![Downloads][downloads-image]][downloads-link] 7 | 8 | Plugin de inclusiones Markdown para Mkdocs. 9 | 10 | > Lee este documento en otros idiomas: 11 | > 12 | > - [Español][es-readme-link] 13 | > - [Français][fr-readme-link] 14 | 15 | ## Instalación 16 | 17 | ```bash 18 | pip install mkdocs-include-markdown-plugin 19 | ``` 20 | 21 | ## Documentación 22 | 23 | ### Preparación 24 | 25 | Habilita el plugin en tu `mkdocs.yml`: 26 | 27 | ```yaml 28 | plugins: 29 | - include-markdown 30 | ``` 31 | 32 | ### Configuración 33 | 34 | El comportamiento global del plugin puede ser personalizado en la configuración. 35 | 36 | La mayoría de los parámetros de configuración definirán los valores por defecto 37 | pasados a los argumentos de las directivas y están documentados en la 38 | [referencia](#referencia). 39 | 40 | ```yaml 41 | plugins: 42 | - include-markdown: 43 | encoding: ascii 44 | preserve_includer_indent: false 45 | dedent: false 46 | trailing_newlines: true 47 | comments: true 48 | rewrite_relative_urls: true 49 | heading_offset: 0 50 | start: 51 | end: 52 | recursive: true 53 | ``` 54 | 55 | #### `opening_tag` y `closing_tag` 56 | 57 | Etiquetas de apertura y cierre por defecto. Cuando no se especifican son `{%` y 58 | `%}`. 59 | 60 | ```yaml 61 | plugins: 62 | - include-markdown: 63 | opening_tag: "{!" 64 | closing_tag: "!}" 65 | ``` 66 | 67 | #### `exclude` 68 | 69 | Patrones de comodín de exclusión globales. Las rutas relativas definidas aquí 70 | serán relativas al directorio [`docs_dir`]. 71 | 72 | ```yaml 73 | plugins: 74 | - include-markdown: 75 | exclude: 76 | - LICENSE.md 77 | - api/** 78 | ``` 79 | 80 | #### `cache` 81 | 82 | Tiempo de caducidad en segundos para las solicitudes HTTP almacenadas en caché al 83 | incluir desde URL. 84 | 85 | ```yaml 86 | plugins: 87 | - include-markdown: 88 | cache: 600 89 | ``` 90 | 91 | Para poder utilizar esta función, se debe instalar la dependencia [platformdirs] 92 | o definir la configuración [`cache_dir`](#cache_dir). Puedes incluir 93 | [platformdirs] en la instalación del plugin agregando el extra `cache`: 94 | 95 | ```txt 96 | # requirements.txt 97 | mkdocs-include-markdown-plugin[cache] 98 | ``` 99 | 100 | #### `cache_dir` 101 | 102 | Directorio donde se almacenarán las solicitudes HTTP en caché. Si se configura, 103 | no es necesario instalar [platformdirs] para usar [`cache`](#cache). 104 | 105 | ```yaml 106 | plugins: 107 | - include-markdown: 108 | cache: 600 109 | cache_dir: ./mkdocs-include-markdown-cache 110 | ``` 111 | 112 | Se agregará un archivo *.gitignore* al directorio de caché si no existe para 113 | evitar confirmar los archivos de caché. 114 | 115 | #### `directives` 116 | 117 | Personaliza los nombres de las directivas. 118 | 119 | ```yaml 120 | plugins: 121 | - include-markdown: 122 | directives: 123 | include-markdown: include-md 124 | include: replace 125 | ``` 126 | 127 | ### Referencia 128 | 129 | Este plugin provee dos directivas, una para incluir archivos Markdown y otra para 130 | incluir archivos de cualquier tipo. 131 | 132 | Las rutas de los archivos a incluir pueden ser: 133 | 134 | - URLs para incluir contenido remoto. 135 | - Archivos locales: 136 | - Rutas absolutas (comenzando con un separador de rutas). 137 | - Relativas desde el archivo que las incluye (empezando por `./` o `../`). 138 | - Relativo al directorio [`docs_dir`]. Por ejemplo, si tu `docs_dir` es 139 | _./docs/_, entonces `includes/header.md` coincidirá con el archivo 140 | *_./docs/includes/header.md_*. 141 | - [Patrones glob de Bash] que coincidan con múltiples archivos locales. 142 | 143 | Las rutas de archivo para incluir y los argumentos de cadena se pueden envolver 144 | con comillas dobles `"` o simples `'`, que se pueden escapar anteponiendo un 145 | carácter `\` como `\"` y `\'`. 146 | 147 | Las cadenas **start** y **end** pueden contener caracteres usuales de secuencias 148 | de escape (al estilo Python) como `\n` para hacer coincidir contra caracteres de 149 | salto de línea. 150 | 151 | #### **`include-markdown`** 152 | 153 | Incluye contenido de archivos Markdown, opcionalmente usando dos delimitadores 154 | para filtrar el contenido a incluir. 155 | 156 | - # 157 | **start**: Delimitador que marca el comienzo del contenido a incluir. 158 | - # **end**: 159 | Delimitador que marca el final del contenido a incluir. 160 | - # 162 | **preserve-includer-indent** (*true*): Cuando esta opción está habilitada (por 163 | defecto), cada línea del contenido a incluir es indentada con el mismo número de 164 | espacios usados para indentar la plantilla `{% %}` incluidora. Los valores 165 | posibles son `true` y `false`. 166 | - # 167 | **dedent** (*false*): Si se habilita, el contenido incluido será dedentado. 168 | - # 169 | **exclude**: Expecifica mediante un glob los archivos que deben ser ignorados. 170 | Sólo es útil pasando globs para incluir múltiples archivos. 171 | - # **trailing-newlines** 173 | (*true*): Cuando esta opción está deshabilitada, los saltos de línea finales que 174 | se encuentran en el contenido a incluir se eliminan. Los valores posibles son 175 | `true` y `false`. 176 | - # 177 | **recursive** (*true*): Cuando esta opción está deshabilitada, los archivos 178 | incluidos no son procesados para incluir de forma recursiva. Los valores 179 | posibles son `true` y `false`. 180 | - # 181 | **encoding** (*'utf-8'*): Especifica la codificación del archivo incluído. Si 182 | no se define, se usará `'utf-8'`. 183 | - # **rewrite-relative-urls** 185 | (*true*): Cuando esta opción está habilitada (por defecto), los enlaces e 186 | imágenes Markdown en el contenido que están definidas mediante una URL relativa 187 | son rescritos para funcionar correctamente en su nueva localización. Los valores 188 | posibles son `true` y `false`. 189 | - # 190 | **comments** (*false*): Cuando esta opción está habilitada, el contenido a 191 | incluir es envuelto por comentarios `` y 192 | `` que ayudan a identificar que el contenido ha sido 193 | incluido. Los valores posibles son `true` y `false`. 194 | - # **heading-offset** (0): 196 | Incrementa o disminuye la profundidad de encabezados Markdown por el número 197 | especificado. Sólo soporta la sintaxis de encabezado de caracteres de hash 198 | (`#`). Acepta valores negativos para eliminar caracteres `#` a la izquierda. 199 | 200 | ##### Ejemplos 201 | 202 | ```jinja 203 | {% 204 | include-markdown "../README.md" 205 | start="" 206 | end="" 207 | %} 208 | ``` 209 | 210 | ```jinja 211 | {% 212 | include-markdown 'includes/header.md' 213 | start='' 214 | end='' 215 | rewrite-relative-urls=false 216 | comments=true 217 | %} 218 | ``` 219 | 220 | ```jinja 221 | {% 222 | include-markdown "includes/header.md" 223 | heading-offset=1 224 | %} 225 | ``` 226 | 227 | ```jinja 228 | {% 229 | include-markdown "../LICENSE*" 230 | start="" 231 | end='' 232 | exclude="../*.rst" 233 | %} 234 | ``` 235 | 236 | ```jinja 237 | {% 238 | include-markdown "**" 239 | exclude="./{index,LICENSE}.md" 240 | %} 241 | ``` 242 | 243 | ```jinja 244 | {% include-markdown '/escap\'ed/single-quotes/in/file\'/name.md' %} 245 | ``` 246 | 247 | #### **`include`** 248 | 249 | Incluye el contenido de un archivo o un grupo de archivos. 250 | 251 | - # **start**: Delimitador que 252 | marca el comienzo del contenido a incluir. 253 | - # **end**: Delimitador que marca 254 | el final del contenido a incluir. 255 | - # **preserve-includer-indent** 257 | (*true*): Cuando esta opción está habilitada (por defecto), cada línea del 258 | contenido a incluir es indentada con el mismo número de espacios usados para 259 | indentar la plantilla `{% %}` incluidora. Los valores posibles son `true` y 260 | `false`. 261 | - # **dedent** (*false*): Si 262 | se habilita, el contenido incluido será dedentado. 263 | - # **exclude**: Especifica 264 | mediante un glob los archivos que deben ser ignorados. Sólo es útil pasando 265 | globs para incluir múltiples archivos. 266 | - # 267 | **trailing-newlines** (*true*): Cuando esta opción está deshabilitada, los 268 | saltos de línea finales que se encuentran en el contenido a incluir se eliminan. 269 | Los valores posibles son `true` y `false`. 270 | - # **recursive** 271 | (*true*): Cuando esta opción está deshabilitada, los archivos incluidos no son 272 | procesados para incluir de forma recursiva. Los valores posibles son `true` y 273 | `false`. 274 | - # **encoding** 275 | (*'utf-8'*): Especifica la codificación del archivo incluído. Si no se define, 276 | se usará `'utf-8'`. 277 | 278 | ##### Ejemplos 279 | 280 | ```jinja 281 | ~~~yaml 282 | {% include "../examples/github-minimal.yml" %} 283 | ~~~ 284 | ``` 285 | 286 | ```jinja 287 | {% 288 | include "../examples.md" 289 | start="~~~yaml" 290 | end="~~~\n" 291 | %} 292 | ``` 293 | 294 | ```jinja 295 | {% 296 | include '**' 297 | exclude='./*.md' 298 | %} 299 | ``` 300 | 301 | ## Agradecimiento 302 | 303 | - [Joe Rickerby] y [contribuidores] por [darme los permisos][cibuildwheel-470] 304 | para [separar este plugin][cibuildwheel-475] de la documentación de 305 | [cibuildwheel][cibuildwheel-repo-link]. 306 | 307 | [Patrones glob de Bash]: https://facelessuser.github.io/wcmatch/glob/#syntax 308 | [pypi-link]: https://pypi.org/project/mkdocs-include-markdown-plugin 309 | [pypi-version-badge-link]: https://img.shields.io/pypi/v/mkdocs-include-markdown-plugin?logo=pypi&logoColor=white 310 | [tests-image]: https://img.shields.io/github/actions/workflow/status/mondeja/mkdocs-include-markdown-plugin/ci.yml?logo=github&label=tests&branch=master 311 | [tests-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/actions?query=workflow%3ACI 312 | [coverage-image]: https://img.shields.io/codecov/c/github/mondeja/mkdocs-include-markdown-plugin?logo=codecov&logoColor=white 313 | [coverage-link]: https://app.codecov.io/gh/mondeja/mkdocs-include-markdown-plugin 314 | [license-image]: https://img.shields.io/pypi/l/mkdocs-include-markdown-plugin?color=light-green&logo=apache&logoColor=white 315 | [license-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/LICENSE 316 | [downloads-image]: https://img.shields.io/pypi/dm/mkdocs-include-markdown-plugin 317 | [downloads-link]: https://pepy.tech/project/mkdocs-include-markdown-plugin 318 | [platformdirs]: https://pypi.org/project/platformdirs/ 319 | [cibuildwheel-470]: https://github.com/pypa/cibuildwheel/issues/470 320 | [cibuildwheel-475]: https://github.com/pypa/cibuildwheel/pull/475 321 | [cibuildwheel-repo-link]: https://github.com/pypa/cibuildwheel 322 | [es-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/es/README.md 323 | [fr-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/fr/README.md 324 | [`docs_dir`]: https://www.mkdocs.org/user-guide/configuration/#docs_dir 325 | [Joe Rickerby]: https://github.com/joerick 326 | [contribuidores]: https://github.com/mondeja/mkdocs-include-markdown-plugin/graphs/contributors 327 | -------------------------------------------------------------------------------- /locale/es/README.md.po: -------------------------------------------------------------------------------- 1 | # 2 | msgid "" 3 | msgstr "" 4 | 5 | msgid "Mkdocs Markdown includer plugin." 6 | msgstr "Plugin de inclusiones Markdown para Mkdocs." 7 | 8 | msgid "Installation" 9 | msgstr "Instalación" 10 | 11 | msgid "Documentation" 12 | msgstr "Documentación" 13 | 14 | msgid "Setup" 15 | msgstr "Preparación" 16 | 17 | msgid "Enable the plugin in your `mkdocs.yml`:" 18 | msgstr "Habilita el plugin en tu `mkdocs.yml`:" 19 | 20 | msgid "Reference" 21 | msgstr "Referencia" 22 | 23 | msgid "" 24 | "Includes Markdown files content, optionally using two delimiters to filter " 25 | "the content to include." 26 | msgstr "" 27 | "Incluye contenido de archivos Markdown, opcionalmente usando dos " 28 | "delimitadores para filtrar el contenido a incluir." 29 | 30 | msgid "Acknowledgment" 31 | msgstr "Agradecimiento" 32 | 33 | msgid "" 34 | "[Joe Rickerby] and [contributors] for [giving me the " 35 | "permissions][cibuildwheel-470] to [separate this plugin][cibuildwheel-475] " 36 | "from the documentation of [cibuildwheel][cibuildwheel-repo-link]." 37 | msgstr "" 38 | "[Joe Rickerby] y [contribuidores] por [darme los permisos][cibuildwheel-470]" 39 | " para [separar este plugin][cibuildwheel-475] de la documentación de " 40 | "[cibuildwheel][cibuildwheel-repo-link]." 41 | 42 | msgid "" 43 | "[pypi-version-badge-link]: https://img.shields.io/pypi/v/mkdocs-include-" 44 | "markdown-plugin?logo=pypi&logoColor=white" 45 | msgstr "" 46 | "[pypi-version-badge-link]: https://img.shields.io/pypi/v/mkdocs-include-" 47 | "markdown-plugin?logo=pypi&logoColor=white" 48 | 49 | msgid "" 50 | "[tests-link]: https://github.com/mondeja/mkdocs-include-markdown-" 51 | "plugin/actions?query=workflow%3ACI" 52 | msgstr "" 53 | "[tests-link]: https://github.com/mondeja/mkdocs-include-markdown-" 54 | "plugin/actions?query=workflow%3ACI" 55 | 56 | msgid "[cibuildwheel-470]: https://github.com/pypa/cibuildwheel/issues/470" 57 | msgstr "[cibuildwheel-470]: https://github.com/pypa/cibuildwheel/issues/470" 58 | 59 | msgid "[cibuildwheel-repo-link]: https://github.com/pypa/cibuildwheel" 60 | msgstr "[cibuildwheel-repo-link]: https://github.com/pypa/cibuildwheel" 61 | 62 | msgid "Read this document in other languages:" 63 | msgstr "Lee este documento en otros idiomas:" 64 | 65 | msgid "" 66 | "[es-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-" 67 | "plugin/blob/master/locale/es/README.md" 68 | msgstr "" 69 | "[es-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-" 70 | "plugin/blob/master/locale/es/README.md" 71 | 72 | msgid "" 73 | "This plugin provides two directives, one to include Markdown files and " 74 | "another to include files of any type." 75 | msgstr "" 76 | "Este plugin provee dos directivas, una para incluir archivos Markdown y otra" 77 | " para incluir archivos de cualquier tipo." 78 | 79 | msgid "Includes the content of a file or a group of files." 80 | msgstr "Incluye el contenido de un archivo o un grupo de archivos." 81 | 82 | msgid "" 83 | "[fr-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-" 84 | "plugin/blob/master/locale/fr/README.md" 85 | msgstr "" 86 | "[fr-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-" 87 | "plugin/blob/master/locale/fr/README.md" 88 | 89 | msgid "" 90 | "# " 91 | "**start**: Delimiter that marks the beginning of the content to include." 92 | msgstr "" 93 | "# " 94 | "**start**: Delimitador que marca el comienzo del contenido a incluir." 95 | 96 | msgid "" 97 | "# " 98 | "**end**: Delimiter that marks the end of the content to include." 99 | msgstr "" 100 | "# " 101 | "**end**: Delimitador que marca el final del contenido a incluir." 102 | 103 | msgid "" 104 | "# **preserve-includer-indent** " 106 | "(*true*): When this option is enabled (default), every line of the content " 107 | "to include is indented with the same number of spaces used to indent the " 108 | "includer `{% %}` template. Possible values are `true` and `false`." 109 | msgstr "" 110 | "# **preserve-includer-indent** " 112 | "(*true*): Cuando esta opción está habilitada (por defecto), cada línea del " 113 | "contenido a incluir es indentada con el mismo número de espacios usados para" 114 | " indentar la plantilla `{% %}` incluidora. Los valores posibles son `true` y" 115 | " `false`." 116 | 117 | msgid "" 118 | "# " 119 | "**dedent** (*false*): If enabled, the included content will be dedented." 120 | msgstr "" 121 | "# " 122 | "**dedent** (*false*): Si se habilita, el contenido incluido será dedentado." 123 | 124 | msgid "" 125 | "# **rewrite-relative-urls** (*true*): " 127 | "When this option is enabled (default), Markdown links and images in the " 128 | "content that are specified by a relative URL are rewritten to work correctly" 129 | " in their new location. Possible values are `true` and `false`." 130 | msgstr "" 131 | "# **rewrite-relative-urls** (*true*): " 133 | "Cuando esta opción está habilitada (por defecto), los enlaces e imágenes " 134 | "Markdown en el contenido que están definidas mediante una URL relativa son " 135 | "rescritos para funcionar correctamente en su nueva localización. Los valores" 136 | " posibles son `true` y `false`." 137 | 138 | msgid "" 139 | "# **comments** (*false*): When this option is " 141 | "enabled, the content to include is wrapped by `` and " 142 | "`` comments which help to identify that the content has " 143 | "been included. Possible values are `true` and `false`." 144 | msgstr "" 145 | "# **comments** (*false*): Cuando esta opción está " 147 | "habilitada, el contenido a incluir es envuelto por comentarios `` y `` que ayudan a identificar que el " 149 | "contenido ha sido incluido. Los valores posibles son `true` y `false`." 150 | 151 | msgid "" 152 | "# **heading-offset** (0): Increases or " 154 | "decreases the Markdown headings depth by this number. Only supports number " 155 | "sign (`#`) heading syntax. Accepts negative values to drop leading `#` " 156 | "characters." 157 | msgstr "" 158 | "# **heading-offset** (0): Incrementa o " 160 | "disminuye la profundidad de encabezados Markdown por el número especificado." 161 | " Sólo soporta la sintaxis de encabezado de caracteres de hash (`#`). Acepta " 162 | "valores negativos para eliminar caracteres `#` a la izquierda." 163 | 164 | msgid "" 165 | "# **exclude**: Specify with a glob which files should" 167 | " be ignored. Only useful when passing globs to include multiple files." 168 | msgstr "" 169 | "# **exclude**: Expecifica mediante un glob los " 171 | "archivos que deben ser ignorados. Sólo es útil pasando globs para incluir " 172 | "múltiples archivos." 173 | 174 | msgid "" 175 | "# **start**: Delimiter" 176 | " that marks the beginning of the content to include." 177 | msgstr "" 178 | "# **start**: " 179 | "Delimitador que marca el comienzo del contenido a incluir." 180 | 181 | msgid "" 182 | "# **end**: Delimiter that " 183 | "marks the end of the content to include." 184 | msgstr "" 185 | "# **end**: Delimitador que" 186 | " marca el final del contenido a incluir." 187 | 188 | msgid "" 189 | "# **preserve-includer-indent** (*true*): When this " 191 | "option is enabled (default), every line of the content to include is " 192 | "indented with the same number of spaces used to indent the includer `{% %}` " 193 | "template. Possible values are `true` and `false`." 194 | msgstr "" 195 | "# **preserve-includer-indent** (*true*): Cuando esta " 197 | "opción está habilitada (por defecto), cada línea del contenido a incluir es " 198 | "indentada con el mismo número de espacios usados para indentar la plantilla " 199 | "`{% %}` incluidora. Los valores posibles son `true` y `false`." 200 | 201 | msgid "" 202 | "# **dedent** " 203 | "(*false*): If enabled, the included content will be dedented." 204 | msgstr "" 205 | "# **dedent** " 206 | "(*false*): Si se habilita, el contenido incluido será dedentado." 207 | 208 | msgid "" 209 | "# **exclude**: " 210 | "Specify with a glob which files should be ignored. Only useful when passing " 211 | "globs to include multiple files." 212 | msgstr "" 213 | "# **exclude**: " 214 | "Especifica mediante un glob los archivos que deben ser ignorados. Sólo es " 215 | "útil pasando globs para incluir múltiples archivos." 216 | 217 | msgid "" 218 | "# **trailing-newlines** (*true*): When this" 220 | " option is disabled, the trailing newlines found in the content to include " 221 | "are stripped. Possible values are `true` and `false`." 222 | msgstr "" 223 | "# **trailing-newlines** (*true*): Cuando " 225 | "esta opción está deshabilitada, los saltos de línea finales que se " 226 | "encuentran en el contenido a incluir se eliminan. Los valores posibles son " 227 | "`true` y `false`." 228 | 229 | msgid "" 230 | "# **trailing-newlines** (*true*): When this option is " 232 | "disabled, the trailing newlines found in the content to include are " 233 | "stripped. Possible values are `true` and `false`." 234 | msgstr "" 235 | "# **trailing-newlines** (*true*): Cuando esta opción está " 237 | "deshabilitada, los saltos de línea finales que se encuentran en el contenido" 238 | " a incluir se eliminan. Los valores posibles son `true` y `false`." 239 | 240 | msgid "" 241 | "The arguments **start** and **end** may contain usual (Python-style) escape " 242 | "sequences like `\\n` to match against newlines." 243 | msgstr "" 244 | "Las cadenas **start** y **end** pueden contener caracteres usuales de " 245 | "secuencias de escape (al estilo Python) como `\\n` para hacer coincidir " 246 | "contra caracteres de salto de línea." 247 | 248 | msgid "" 249 | "File paths to include and string arguments can be wrapped by double `\"` or " 250 | "single `'` quotes, which can be escaped prepending them a `\\` character as " 251 | "`\\\"` and `\\'`." 252 | msgstr "" 253 | "Las rutas de archivo para incluir y los argumentos de cadena se pueden " 254 | "envolver con comillas dobles `\"` o simples `'`, que se pueden escapar " 255 | "anteponiendo un carácter `\\` como `\\\"` y `\\'`." 256 | 257 | msgid "" 258 | "# **encoding** (*'utf-8'*): Specify the encoding of " 260 | "the included file. If not defined `'utf-8'` will be used." 261 | msgstr "" 262 | "# **encoding** (*'utf-8'*): Especifica la " 264 | "codificación del archivo incluído. Si no se define, se usará `'utf-8'`." 265 | 266 | msgid "" 267 | "# **encoding** " 268 | "(*'utf-8'*): Specify the encoding of the included file. If not defined " 269 | "`'utf-8'` will be used." 270 | msgstr "" 271 | "# **encoding** " 272 | "(*'utf-8'*): Especifica la codificación del archivo incluído. Si no se " 273 | "define, se usará `'utf-8'`." 274 | 275 | msgid "Configuration" 276 | msgstr "Configuración" 277 | 278 | msgid "" 279 | "The global behaviour of the plugin can be customized in the configuration." 280 | msgstr "" 281 | "El comportamiento global del plugin puede ser personalizado en la " 282 | "configuración." 283 | 284 | msgid "" 285 | "[coverage-image]: https://img.shields.io/codecov/c/github/mondeja/mkdocs-" 286 | "include-markdown-plugin?logo=codecov&logoColor=white" 287 | msgstr "" 288 | "[coverage-image]: https://img.shields.io/codecov/c/github/mondeja/mkdocs-" 289 | "include-markdown-plugin?logo=codecov&logoColor=white" 290 | 291 | msgid "" 292 | "[coverage-link]: https://app.codecov.io/gh/mondeja/mkdocs-include-markdown-" 293 | "plugin" 294 | msgstr "" 295 | "[coverage-link]: https://app.codecov.io/gh/mondeja/mkdocs-include-markdown-" 296 | "plugin" 297 | 298 | msgid "" 299 | "[tests-image]: " 300 | "https://img.shields.io/github/actions/workflow/status/mondeja/mkdocs-" 301 | "include-markdown-plugin/ci.yml?logo=github&label=tests&branch=master" 302 | msgstr "" 303 | "[tests-image]: " 304 | "https://img.shields.io/github/actions/workflow/status/mondeja/mkdocs-" 305 | "include-markdown-plugin/ci.yml?logo=github&label=tests&branch=master" 306 | 307 | msgid "" 308 | "[license-image]: https://img.shields.io/pypi/l/mkdocs-include-markdown-" 309 | "plugin?color=light-green&logo=apache&logoColor=white" 310 | msgstr "" 311 | "[license-image]: https://img.shields.io/pypi/l/mkdocs-include-markdown-" 312 | "plugin?color=light-green&logo=apache&logoColor=white" 313 | 314 | msgid "" 315 | "[license-link]: https://github.com/mondeja/mkdocs-include-markdown-" 316 | "plugin/blob/master/LICENSE" 317 | msgstr "" 318 | "[license-link]: https://github.com/mondeja/mkdocs-include-markdown-" 319 | "plugin/blob/master/LICENSE" 320 | 321 | msgid "[platformdirs]: https://pypi.org/project/platformdirs/" 322 | msgstr "[platformdirs]: https://pypi.org/project/platformdirs/" 323 | 324 | msgid "" 325 | "Most of the settings will define the default values passed to arguments of " 326 | "directives and are documented in the [reference](#reference)." 327 | msgstr "" 328 | "La mayoría de los parámetros de configuración definirán los valores por " 329 | "defecto pasados a los argumentos de las directivas y están documentados en " 330 | "la [referencia](#referencia)." 331 | 332 | msgid "Paths of included files can be either:" 333 | msgstr "Las rutas de los archivos a incluir pueden ser:" 334 | 335 | msgid "URLs to include remote content." 336 | msgstr "URLs para incluir contenido remoto." 337 | 338 | msgid "Local files:" 339 | msgstr "Archivos locales:" 340 | 341 | msgid "Absolute paths (starting with a path separator)." 342 | msgstr "Rutas absolutas (comenzando con un separador de rutas)." 343 | 344 | msgid "[Bash wildcard globs] matching multiple local files." 345 | msgstr "[Patrones glob de Bash] que coincidan con múltiples archivos locales." 346 | 347 | msgid "" 348 | "Relative from the file that includes them (starting with `./` or `../`)." 349 | msgstr "" 350 | "Relativas desde el archivo que las incluye (empezando por `./` o `../`)." 351 | 352 | msgid "`opening_tag` and `closing_tag`" 353 | msgstr "`opening_tag` y `closing_tag`" 354 | 355 | msgid "" 356 | "Default opening and closing tags. When not specified they are `{%` and `%}`." 357 | msgstr "" 358 | "Etiquetas de apertura y cierre por defecto. Cuando no se especifican son " 359 | "`{%` y `%}`." 360 | 361 | msgid "" 362 | "Global exclusion wildcard patterns. Relative paths defined here will be " 363 | "relative to the [`docs_dir`] directory." 364 | msgstr "" 365 | "Patrones de comodín de exclusión globales. Las rutas relativas definidas " 366 | "aquí serán relativas al directorio [`docs_dir`]." 367 | 368 | msgid "" 369 | "Expiration time in seconds for cached HTTP requests when including from " 370 | "URLs." 371 | msgstr "" 372 | "Tiempo de caducidad en segundos para las solicitudes HTTP almacenadas en " 373 | "caché al incluir desde URL." 374 | 375 | msgid "" 376 | "[`docs_dir`]: https://www.mkdocs.org/user-guide/configuration/#docs_dir" 377 | msgstr "" 378 | "[`docs_dir`]: https://www.mkdocs.org/user-guide/configuration/#docs_dir" 379 | 380 | msgid "Examples" 381 | msgstr "Ejemplos" 382 | 383 | msgid "" 384 | "Relative to the [`docs_dir`] directory. For instance if your `docs_dir` is " 385 | "*./docs/*, then `includes/header.md` will match the file " 386 | "*./docs/includes/header.md*." 387 | msgstr "" 388 | "Relativo al directorio [`docs_dir`]. Por ejemplo, si tu `docs_dir` es " 389 | "_./docs/_, entonces `includes/header.md` coincidirá con el archivo " 390 | "*_./docs/includes/header.md_*." 391 | 392 | msgid "" 393 | "[Bash wildcard globs]: https://facelessuser.github.io/wcmatch/glob/#syntax" 394 | msgstr "" 395 | "[Patrones glob de Bash]: https://facelessuser.github.io/wcmatch/glob/#syntax" 396 | 397 | msgid "[pypi-link]: https://pypi.org/project/mkdocs-include-markdown-plugin" 398 | msgstr "[pypi-link]: https://pypi.org/project/mkdocs-include-markdown-plugin" 399 | 400 | msgid "" 401 | "# " 402 | "**recursive** (*true*): When this option is disabled, included files are not" 403 | " processed for recursive includes. Possible values are `true` and `false`." 404 | msgstr "" 405 | "# " 406 | "**recursive** (*true*): Cuando esta opción está deshabilitada, los archivos " 407 | "incluidos no son procesados para incluir de forma recursiva. Los valores " 408 | "posibles son `true` y `false`." 409 | 410 | msgid "" 411 | "# **recursive** (*true*): When this option is " 413 | "disabled, included files are not processed for recursive includes. Possible " 414 | "values are `true` and `false`." 415 | msgstr "" 416 | "# **recursive** (*true*): Cuando esta opción está " 418 | "deshabilitada, los archivos incluidos no son procesados para incluir de " 419 | "forma recursiva. Los valores posibles son `true` y `false`." 420 | 421 | msgid "[Joe Rickerby]: https://github.com/joerick" 422 | msgstr "[Joe Rickerby]: https://github.com/joerick" 423 | 424 | msgid "" 425 | "[contributors]: https://github.com/mondeja/mkdocs-include-markdown-" 426 | "plugin/graphs/contributors" 427 | msgstr "" 428 | "[contribuidores]: https://github.com/mondeja/mkdocs-include-markdown-" 429 | "plugin/graphs/contributors" 430 | 431 | msgid "[cibuildwheel-475]: https://github.com/pypa/cibuildwheel/pull/475" 432 | msgstr "[cibuildwheel-475]: https://github.com/pypa/cibuildwheel/pull/475" 433 | 434 | msgid "" 435 | "[downloads-image]: https://img.shields.io/pypi/dm/mkdocs-include-markdown-" 436 | "plugin" 437 | msgstr "" 438 | "[downloads-image]: https://img.shields.io/pypi/dm/mkdocs-include-markdown-" 439 | "plugin" 440 | 441 | msgid "" 442 | "[downloads-link]: https://pepy.tech/project/mkdocs-include-markdown-plugin" 443 | msgstr "" 444 | "[downloads-link]: https://pepy.tech/project/mkdocs-include-markdown-plugin" 445 | 446 | msgid "Customize the names of the directives." 447 | msgstr "Personaliza los nombres de las directivas." 448 | 449 | msgid "" 450 | "In order to use this feature, the dependency [platformdirs] must be " 451 | "installed or the setting [`cache_dir`](#cache_dir) must be defined. You can " 452 | "include [platformdirs] in the installation of the plugin adding the `cache` " 453 | "extra:" 454 | msgstr "" 455 | "Para poder utilizar esta función, se debe instalar la dependencia " 456 | "[platformdirs] o definir la configuración [`cache_dir`](#cache_dir). Puedes " 457 | "incluir [platformdirs] en la instalación del plugin agregando el extra " 458 | "`cache`:" 459 | 460 | msgid "" 461 | "Directory where cached HTTP requests will be stored. If set, [platformdirs] " 462 | "is not needed to be installed to use [`cache`](#cache)." 463 | msgstr "" 464 | "Directorio donde se almacenarán las solicitudes HTTP en caché. Si se " 465 | "configura, no es necesario instalar [platformdirs] para usar " 466 | "[`cache`](#cache)." 467 | 468 | msgid "" 469 | "A *.gitignore* file will be added to the cache directory if not exists to " 470 | "avoid committing the cache files." 471 | msgstr "" 472 | "Se agregará un archivo *.gitignore* al directorio de caché si no existe para" 473 | " evitar confirmar los archivos de caché." 474 | -------------------------------------------------------------------------------- /locale/fr/README.md: -------------------------------------------------------------------------------- 1 | # mkdocs-include-markdown-plugin 2 | 3 | [![PyPI][pypi-version-badge-link]][pypi-link] 4 | [![License][license-image]][license-link] [![Tests][tests-image]][tests-link] 5 | [![Coverage status][coverage-image]][coverage-link] 6 | [![Downloads][downloads-image]][downloads-link] 7 | 8 | Plugin d'inclusion de Markdown pour Mkdocs. 9 | 10 | > Lire ce document dans d'autres langues: 11 | > 12 | > - [Español][es-readme-link] 13 | > - [Français][fr-readme-link] 14 | 15 | ## Installation 16 | 17 | ```bash 18 | pip install mkdocs-include-markdown-plugin 19 | ``` 20 | 21 | ## Documentation 22 | 23 | ### Préparation 24 | 25 | Activer le plugin dans votre fichier `mkdocs.yml`: 26 | 27 | ```yaml 28 | plugins: 29 | - include-markdown 30 | ``` 31 | 32 | ### Configuration 33 | 34 | Le comportement global du plugin peut être personnalisé dans la configuration. 35 | 36 | La plupart des paramètres définissent les valeurs par défaut transmises aux 37 | arguments des directives et sont documentés dans la [référence](#référence). 38 | 39 | ```yaml 40 | plugins: 41 | - include-markdown: 42 | encoding: ascii 43 | preserve_includer_indent: false 44 | dedent: false 45 | trailing_newlines: true 46 | comments: true 47 | rewrite_relative_urls: true 48 | heading_offset: 0 49 | start: 50 | end: 51 | recursive: true 52 | ``` 53 | 54 | #### `opening_tag` et `closing_tag` 55 | 56 | Balises d'ouverture et de fermeture par défaut. Lorsqu'elles ne sont pas 57 | spécifiées, elles sont `{%` et `%}`. 58 | 59 | ```yaml 60 | plugins: 61 | - include-markdown: 62 | opening_tag: "{!" 63 | closing_tag: "!}" 64 | ``` 65 | 66 | #### `exclude` 67 | 68 | Modèles de globes d'exclusion globaux. Les chemins relatifs définis ici seront 69 | relatifs au répertoire [`docs_dir`]. 70 | 71 | ```yaml 72 | plugins: 73 | - include-markdown: 74 | exclude: 75 | - LICENSE.md 76 | - api/** 77 | ``` 78 | 79 | #### `cache` 80 | 81 | Délai d'expiration en secondes pour les requêtes HTTP mises en cache lors de 82 | l'inclusion d'URL. 83 | 84 | ```yaml 85 | plugins: 86 | - include-markdown: 87 | cache: 600 88 | ``` 89 | 90 | Pour utiliser cette fonctionnalité, la dépendance [platformdirs] doit être 91 | installée ou le paramètre [`cache_dir`](#cache_dir) doit être défini. Vous 92 | pouvez inclure [platformdirs] dans l'installation du plugin en ajoutant le 93 | supplément `cache` : 94 | 95 | ```txt 96 | # requirements.txt 97 | mkdocs-include-markdown-plugin[cache] 98 | ``` 99 | 100 | #### `cache_dir` 101 | 102 | Répertoire dans lequel les requêtes HTTP mises en cache seront stockées. Si 103 | défini, [platformdirs] n'a pas besoin d'être installé pour utiliser 104 | [`cache`](#cache). 105 | 106 | ```yaml 107 | plugins: 108 | - include-markdown: 109 | cache: 600 110 | cache_dir: ./mkdocs-include-markdown-cache 111 | ``` 112 | 113 | Un fichier *.gitignore* sera ajouté au répertoire de cache s'il n'existe pas pour 114 | éviter de valider les fichiers de cache. 115 | 116 | #### `directives` 117 | 118 | Personnaliser les noms des directives. 119 | 120 | ```yaml 121 | plugins: 122 | - include-markdown: 123 | directives: 124 | include-markdown: include-md 125 | include: replace 126 | ``` 127 | 128 | ### Référence 129 | 130 | Ce plugin fournit deux directives, une pour inclure des fichiers Markdown et une 131 | autre pour inclure des fichiers de tout type. 132 | 133 | Les chemins des fichiers inclus peuvent être soit: 134 | 135 | - URL pour inclure du contenu distant. 136 | - Fichiers locaux: 137 | - Chemins de fichier absolus (commençant par un séparateur de chemin). 138 | - Relatifs du fichiers qui les inclut (commençant par `./` ou `../`). 139 | - Relatif au répertoire [`docs_dir`]. Par exemple, si votre `docs_dir` est 140 | _./docs/_, alors `includes/header.md` va correspondre au fichier 141 | *_./docs/includes/header.md_*. 142 | - [Globs génériques Bash] correspondant à plusieurs fichiers locaux. 143 | 144 | Les chemins d'accès aux fichiers à inclure et les arguments de chaîne peuvent 145 | être entourés de guillemets doubles `"` ou simples `'`, qui peuvent être 146 | échappés en leur ajoutant un caractère `\` comme `\"` et `\'`. 147 | 148 | Les chaînes **start** et **end** peuvent contenir des séquences d'échappement 149 | habituelles (de style Python) telles que `\n` pour correspondre aux nouvelles 150 | lignes. 151 | 152 | #### **`include-markdown`** 153 | 154 | Inclut contenu des Markdown fichiers, en utilisant éventuellement deux 155 | délimiteurs pour filtrer le contenu à inclure. 156 | 157 | - # 158 | **start**: Délimiteur qui marque le début du contenu à inclure. 159 | - # **end**: 160 | Délimiteur qui marque la fin du contenu à inclure. 161 | - # 163 | **preserve-includer-indent** (*true*): Lorsque cette option est activée (par 164 | défaut), chaque ligne du contenu à inclure est indentée avec le même nombre 165 | d'espaces utilisé pour indenter l'incluseur modèle `{% %}`. Les valeurs 166 | possibles sont `true` et `false`. 167 | - # 168 | **dedent** (*false*): Lorsque est activée, le contenu inclus sera déchiqueté. 169 | - # 170 | **exclude**: Spécifiez avec un glob quels fichiers doivent être ignorés. 171 | Uniquement utile lors du passage de globs pour inclure plusieurs fichiers. 172 | - # **trailing-newlines** 174 | (*true*): Lorsque cette option est désactivée, les nouvelles lignes de fin 175 | trouvées dans le contenu à inclure sont supprimées. Les valeurs possibles sont 176 | `true` et `false`. 177 | - # 178 | **recursive** (*true*): Lorsque cette option est désactivée, les fichiers 179 | inclus ne sont pas traités pour des inclusions récursives. Les valeurs possibles 180 | sont `true` et `false`. 181 | - # 182 | **encoding** (*'utf-8'*): Spécifiez l'encodage du fichier inclus. S'il n'est 183 | pas défini, `'utf-8'` sera utilisé. 184 | - # **rewrite-relative-urls** 186 | (*true*): Lorsque cette option est activée (par défaut), liens et images 187 | Markdown dans le contenu qui sont spécifiés par une URL relative sont réécrits 188 | pour fonctionner correctement dans leur nouvel emplacement. Les valeurs 189 | possibles sont `true` et `false`. 190 | - # 191 | **comments** (*false*): Lorsque cette option est activée, le contenu à inclure 192 | est entouré de `` et `` commentaires 193 | qui aident à identifier que le contenu a été inclus. Les valeurs possibles sont 194 | `true` et `false`. 195 | - # **heading-offset** (0): Augmente 197 | ou diminue la profondeur des en-têtes Markdown de ce nombre. Ne prend en charge 198 | que la syntaxe d'en-tête du signe dièse (`#`). Cet argument accepte les valeurs 199 | négatives pour supprimer les caractères `#` de tête. 200 | 201 | ##### Exemples 202 | 203 | ```jinja 204 | {% 205 | include-markdown "../README.md" 206 | start="" 207 | end="" 208 | %} 209 | ``` 210 | 211 | ```jinja 212 | {% 213 | include-markdown 'includes/header.md' 214 | start='' 215 | end='' 216 | rewrite-relative-urls=false 217 | comments=true 218 | %} 219 | ``` 220 | 221 | ```jinja 222 | {% 223 | include-markdown "includes/header.md" 224 | heading-offset=1 225 | %} 226 | ``` 227 | 228 | ```jinja 229 | {% 230 | include-markdown "../LICENSE*" 231 | start="" 232 | end='' 233 | exclude="../*.rst" 234 | %} 235 | ``` 236 | 237 | ```jinja 238 | {% 239 | include-markdown "**" 240 | exclude="./{index,LICENSE}.md" 241 | %} 242 | ``` 243 | 244 | ```jinja 245 | {% include-markdown '/escap\'ed/single-quotes/in/file\'/name.md' %} 246 | ``` 247 | 248 | #### **`include`** 249 | 250 | Inclus le contenu d'un fichier ou d'un groupe de fichiers. 251 | 252 | - # **start**: Délimiteur qui 253 | marque le début du contenu à inclure. 254 | - # **end**: Délimiteur qui marque 255 | la fin du contenu à inclure. 256 | - # **preserve-includer-indent** 258 | (*true*): Lorsque cette option est activée (par défaut), chaque ligne du contenu 259 | à inclure est indentée avec le même nombre d'espaces utilisé pour indenter 260 | l'incluseur modèle `{% %}`. Les valeurs possibles sont `true` et `false`. 261 | - # **dedent** (*false*): 262 | Lorsque est activée, le contenu inclus sera déchiqueté. 263 | - # **exclude**: Spécifiez 264 | avec un glob quels fichiers doivent être ignorés. Uniquement utile lors du 265 | passage de globs pour inclure plusieurs fichiers. 266 | - # 267 | **trailing-newlines** (*true*): Lorsque cette option est désactivée, les 268 | nouvelles lignes de fin trouvées dans le contenu à inclure sont supprimées. Les 269 | valeurs possibles sont `true` et `false`. 270 | - # **recursive** 271 | (*true*): Lorsque cette option est désactivée, les fichiers inclus ne sont pas 272 | traités pour des inclusions récursives. Les valeurs possibles sont `true` et 273 | `false`. 274 | - # **encoding** 275 | (*'utf-8'*): Spécifiez l'encodage du fichier inclus. S'il n'est pas défini, 276 | `'utf-8'` sera utilisé. 277 | 278 | ##### Exemples 279 | 280 | ```jinja 281 | ~~~yaml 282 | {% include "../examples/github-minimal.yml" %} 283 | ~~~ 284 | ``` 285 | 286 | ```jinja 287 | {% 288 | include "../examples.md" 289 | start="~~~yaml" 290 | end="~~~\n" 291 | %} 292 | ``` 293 | 294 | ```jinja 295 | {% 296 | include '**' 297 | exclude='./*.md' 298 | %} 299 | ``` 300 | 301 | ## Reconnaissance 302 | 303 | - [Joe Rickerby] et [des contributeurs] pour [m'avoir donné les 304 | autorisations][cibuildwheel-470] pour [séparer ce plugin][cibuildwheel-475] de 305 | la documentation de [cibuildwheel][cibuildwheel-repo-link]. 306 | 307 | [Globs génériques Bash]: https://facelessuser.github.io/wcmatch/glob/#syntax 308 | [pypi-link]: https://pypi.org/project/mkdocs-include-markdown-plugin 309 | [pypi-version-badge-link]: https://img.shields.io/pypi/v/mkdocs-include-markdown-plugin?logo=pypi&logoColor=white 310 | [tests-image]: https://img.shields.io/github/actions/workflow/status/mondeja/mkdocs-include-markdown-plugin/ci.yml?logo=github&label=tests&branch=master 311 | [tests-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/actions?query=workflow%3ACI 312 | [coverage-image]: https://img.shields.io/codecov/c/github/mondeja/mkdocs-include-markdown-plugin?logo=codecov&logoColor=white 313 | [coverage-link]: https://app.codecov.io/gh/mondeja/mkdocs-include-markdown-plugin 314 | [license-image]: https://img.shields.io/pypi/l/mkdocs-include-markdown-plugin?color=light-green&logo=apache&logoColor=white 315 | [license-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/LICENSE 316 | [downloads-image]: https://img.shields.io/pypi/dm/mkdocs-include-markdown-plugin 317 | [downloads-link]: https://pepy.tech/project/mkdocs-include-markdown-plugin 318 | [platformdirs]: https://pypi.org/project/platformdirs/ 319 | [cibuildwheel-470]: https://github.com/pypa/cibuildwheel/issues/470 320 | [cibuildwheel-475]: https://github.com/pypa/cibuildwheel/pull/475 321 | [cibuildwheel-repo-link]: https://github.com/pypa/cibuildwheel 322 | [es-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/es/README.md 323 | [fr-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/fr/README.md 324 | [`docs_dir`]: https://www.mkdocs.org/user-guide/configuration/#docs_dir 325 | [Joe Rickerby]: https://github.com/joerick 326 | [des contributeurs]: https://github.com/mondeja/mkdocs-include-markdown-plugin/graphs/contributors 327 | -------------------------------------------------------------------------------- /lychee.toml: -------------------------------------------------------------------------------- 1 | exclude = [ 2 | '^http://127\.0\.0\.1:8088/$', 3 | '^https://img\.shields\.io/github/actions/workflow/status/mondeja/mkdocs-', 4 | '^https://github.com/mondeja/mkdocs-include-markdown-', 5 | 'https://raw\.githubusercontent\.com/mondeja/', 6 | 'https://github.com/user/repo' 7 | ] 8 | exclude_path = ["lychee.toml", "^locale"] 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mkdocs-include-markdown-plugin" 3 | version = "7.1.5" 4 | description = "Mkdocs Markdown includer plugin." 5 | readme = "README.md" 6 | license = "Apache-2.0" 7 | requires-python = ">=3.9" 8 | classifiers = [ 9 | "Development Status :: 5 - Production/Stable", 10 | "Operating System :: OS Independent", 11 | "License :: OSI Approved :: Apache Software License", 12 | "Intended Audience :: Developers", 13 | "Topic :: Documentation", 14 | "Topic :: Software Development :: Documentation", 15 | "Topic :: Text Processing", 16 | "Topic :: Text Processing :: Markup :: Markdown", 17 | "Environment :: Console", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | ] 24 | keywords = ["markdown", "mkdocs", "includer", "plugin"] 25 | dependencies = [ 26 | "mkdocs>=1.4", 27 | "wcmatch" 28 | ] 29 | 30 | [[project.authors]] 31 | name = "Joe Rickerby" 32 | 33 | [[project.authors]] 34 | name = "Álvaro Mondéjar Rubio" 35 | email = "mondejar1994@gmail.com" 36 | 37 | [[project.maintainers]] 38 | name = "Álvaro Mondéjar Rubio" 39 | email = "mondejar1994@gmail.com" 40 | 41 | [project.urls] 42 | Source = "https://github.com/mondeja/mkdocs-include-markdown-plugin" 43 | Documentation = "https://github.com/mondeja/mkdocs-include-markdown-plugin#documentation" 44 | "Bug tracker" = "https://github.com/mondeja/mkdocs-include-markdown-plugin/issues" 45 | Changelog = "https://github.com/mondeja/mkdocs-include-markdown-plugin/releases" 46 | 47 | [project.entry-points."mkdocs.plugins"] 48 | include-markdown = "mkdocs_include_markdown_plugin.plugin:IncludeMarkdownPlugin" 49 | 50 | [project.optional-dependencies] 51 | cache = ["platformdirs"] 52 | 53 | [tool.hatch.build] 54 | include = ["/src"] 55 | 56 | [tool.hatch.build.targets.wheel] 57 | packages = ["src/mkdocs_include_markdown_plugin"] 58 | 59 | [tool.hatch.envs.default] 60 | python = "3.10" 61 | dependencies = ["mondeja-bump"] 62 | 63 | [tool.hatch.envs.style] 64 | python = "3.10" 65 | detached = true 66 | dependencies = ["pre-commit"] 67 | 68 | [tool.hatch.envs.style.scripts] 69 | lint = "pre-commit run -a" 70 | 71 | [tool.hatch.envs.tests] 72 | matrix-name-format = "{variable}-{value}" 73 | dependencies = ["pytest~=7.0", "coverage~=6.4", "covdefaults"] 74 | 75 | [[tool.hatch.envs.tests.matrix]] 76 | python = ["py39", "py310", "py311", "py312", "py313"] 77 | mkdocs = ["1.4.0", "1.4.3", "1.5.0", "1.5.3", "1.6.0"] 78 | cache = ["yes", "no"] 79 | 80 | [tool.hatch.envs.tests.overrides] 81 | matrix.mkdocs.dependencies = [ 82 | { value = "mkdocs==1.4.0", if = ["1.4.0"] }, 83 | { value = "mkdocs==1.4.3", if = ["1.4.3"] }, 84 | { value = "mkdocs==1.5.0", if = ["1.5.0"] }, 85 | { value = "mkdocs==1.5.3", if = ["1.5.3"] }, 86 | { value = "mkdocs==1.6.0", if = ["1.6.0"] }, 87 | ] 88 | matrix.cache.dependencies = [{ value = "platformdirs", if = ["yes"] }] 89 | 90 | [tool.hatch.envs.tests.scripts] 91 | all = "coverage run -m pytest" 92 | unit = "coverage run -m pytest tests/test_unit" 93 | integration = "pytest tests/test_integration --override-ini addopts=-svv" 94 | cov = [ 95 | "hatch run +py=py310 tests:all", 96 | "coverage html", 97 | "python -c 'import webbrowser as w;w.open(\"http://127.0.0.1:8088\")'", 98 | "python -m http.server 8088 -b localhost -d htmlcov", 99 | ] 100 | 101 | [tool.bump] 102 | targets = [{ file = "pyproject.toml" }] 103 | 104 | [tool.project-config] 105 | cache = "2 days" 106 | style = [ 107 | "gh://mondeja/project-config-styles@v5.5/base/pre-commit/md2po2md.json5", 108 | "gh://mondeja/project-config-styles@v5.5/python/base.json5", 109 | "gh://mondeja/project-config-styles@v5.5/python/mypy.json5", 110 | ] 111 | 112 | [tool.coverage.run] 113 | source = ["src"] 114 | plugins = ["covdefaults"] 115 | parallel = true 116 | data_file = ".coverage/.coverage" 117 | 118 | [tool.coverage.report] 119 | exclude_lines = ["def __repr__\\(", "@(abc\\.)?abstractmethod"] 120 | fail_under = 1 121 | 122 | [tool.ruff] 123 | line-length = 80 124 | target-version = "py39" 125 | 126 | [tool.ruff.lint] 127 | select = [ 128 | "W", 129 | "B", 130 | "E", 131 | "I", 132 | "F", 133 | "A", 134 | "D", 135 | "G", 136 | "Q", 137 | "PL", 138 | "UP", 139 | "PT", 140 | "C4", 141 | "EXE", 142 | "ISC", 143 | "T20", 144 | "INP", 145 | "ARG", 146 | "SIM", 147 | "RET", 148 | "FBT", 149 | "ERA", 150 | "T10", 151 | "COM", 152 | "SLOT", 153 | ] 154 | ignore = ["G004"] 155 | 156 | [tool.ruff.lint.pydocstyle] 157 | convention = "google" 158 | 159 | [tool.ruff.lint.flake8-quotes] 160 | inline-quotes = "single" 161 | multiline-quotes = "single" 162 | 163 | [tool.ruff.lint.flake8-pytest-style] 164 | fixture-parentheses = false 165 | parametrize-values-type = "tuple" 166 | parametrize-values-row-type = "tuple" 167 | 168 | [tool.ruff.lint.isort] 169 | lines-after-imports = 2 170 | combine-as-imports = true 171 | force-wrap-aliases = true 172 | known-first-party = ["mkdocs_include_markdown_plugin", "testing_helpers"] 173 | known-local-folder = ["tests"] 174 | required-imports = ["from __future__ import annotations"] 175 | extra-standard-library = [ 176 | "zoneinfo", 177 | "graphlib", 178 | "tomllib", 179 | "wsgiref.types" 180 | ] 181 | 182 | [tool.ruff.lint.per-file-ignores] 183 | "tests/**" = [ 184 | "I002", 185 | "D100", 186 | "D101", 187 | "D102", 188 | "D103", 189 | "D104", 190 | "D107", 191 | "D205", 192 | "D415", 193 | "INP001", 194 | "PLR0913", 195 | "PLR2004", 196 | ] 197 | "setup.py" = ["D205", "INP001", "I002"] 198 | "src/mkdocs_include_markdown_plugin/plugin.py" = [ 199 | "D100", 200 | "D101", 201 | "D102", 202 | ] 203 | 204 | [tool.mypy] 205 | strict = true 206 | python_version = "3.12" 207 | allow_untyped_calls = true 208 | allow_any_generics = true 209 | 210 | [build-system] 211 | requires = ["hatchling"] 212 | build-backend = "hatchling.build" 213 | -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-07/schema", 3 | "title": "Mkdocs Markdown includer plugin.", 4 | "oneOf": [ 5 | { 6 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#readme", 7 | "enum": ["include-markdown"] 8 | }, 9 | { 10 | "type": "object", 11 | "properties": { 12 | "include-markdown": { 13 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#documentation", 14 | "type": "object", 15 | "properties": { 16 | "opening_tag": { 17 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin?tab=readme-ov-file#opening_tag-and-closing_tag", 18 | "type": "string", 19 | "default": "{%" 20 | }, 21 | "closing_tag": { 22 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin?tab=readme-ov-file#opening_tag-and-closing_tag", 23 | "type": "string", 24 | "default": "%}" 25 | }, 26 | "encoding": { 27 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include_encoding", 28 | "type": "string", 29 | "default": "utf-8" 30 | }, 31 | "preserve_includer_indent": { 32 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include_preserve-includer-indent", 33 | "type": "boolean", 34 | "default": true 35 | }, 36 | "dedent": { 37 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include_dedent", 38 | "type": "boolean", 39 | "default": false 40 | }, 41 | "trailing_newlines": { 42 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include_trailing-newlines", 43 | "type": "boolean", 44 | "default": true 45 | }, 46 | "comments": { 47 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include-markdown_comments", 48 | "type": "boolean", 49 | "default": true 50 | }, 51 | "rewrite_relative_urls": { 52 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include-markdown_trailing-newlines", 53 | "type": "boolean", 54 | "default": true 55 | }, 56 | "heading_offset": { 57 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include-markdown_heading-offset", 58 | "type": "integer", 59 | "default": 0 60 | }, 61 | "start": { 62 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include-markdown_start", 63 | "type": "string", 64 | "default": "" 65 | }, 66 | "end": { 67 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include-markdown_end", 68 | "type": "string", 69 | "default": "" 70 | }, 71 | "recursive": { 72 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include_recursive", 73 | "type": "boolean", 74 | "default": true 75 | }, 76 | "exclude": { 77 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin?tab=readme-ov-file#exclude", 78 | "type": "array", 79 | "items": { 80 | "type": "string" 81 | } 82 | }, 83 | "cache": { 84 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin?tab=readme-ov-file#cache", 85 | "type": "integer", 86 | "default": 0 87 | }, 88 | "cache_dir": { 89 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin?tab=readme-ov-file#cache_dir", 90 | "type": "string", 91 | "default": ".mkdocs-include-markdown-cache" 92 | }, 93 | "directives": { 94 | "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin?tab=readme-ov-file#directives", 95 | "type": "object", 96 | "default": { 97 | "include": "include", 98 | "include-markdown": "include-markdown" 99 | } 100 | } 101 | }, 102 | "additionalProperties": false 103 | } 104 | }, 105 | "additionalProperties": false 106 | } 107 | ] 108 | } 109 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Installation using setup.py is no longer supported. 2 | Use `pip install .` instead. 3 | """ 4 | 5 | import sys 6 | 7 | from setuptools import setup 8 | 9 | 10 | sys.exit(__doc__) 11 | 12 | # Fake reference so GitHub still considers it a real package 13 | # for statistics purposes: 14 | setup( 15 | name='mkdocs-include-markdown-plugin', 16 | ) 17 | -------------------------------------------------------------------------------- /src/mkdocs_include_markdown_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | """Mkdocs Markdown plugin to include files.""" 2 | from __future__ import annotations 3 | 4 | 5 | __all__ = ['IncludeMarkdownPlugin'] 6 | 7 | from mkdocs_include_markdown_plugin.plugin import IncludeMarkdownPlugin 8 | -------------------------------------------------------------------------------- /src/mkdocs_include_markdown_plugin/cache.py: -------------------------------------------------------------------------------- 1 | """Persistent file cache.""" 2 | 3 | from __future__ import annotations 4 | 5 | import hashlib 6 | import os 7 | import stat 8 | import time 9 | from importlib.util import find_spec 10 | 11 | 12 | class Cache: 13 | """Cache for arbitrary content, one file per entry.""" 14 | 15 | def __init__( # noqa: D107 16 | self, 17 | cache_dir: str, 18 | expiration_seconds: int = 0, 19 | ): 20 | self.cache_dir = cache_dir 21 | self.expiration_seconds = expiration_seconds 22 | 23 | def get_creation_time_from_fpath(self, fpath: str) -> int: 24 | """Get creation time of an entry in the cache given its path.""" 25 | with open(fpath, 'rb') as f: 26 | return int(f.readline()) 27 | 28 | @classmethod 29 | def generate_unique_key_from_url(cls, url: str) -> str: 30 | """Generate a unique key from an URL.""" 31 | return hashlib.blake2b(url.encode(), digest_size=16).digest().hex() 32 | 33 | def read_file(self, fpath: str, encoding: str = 'utf-8') -> str: # noqa: D102 34 | f = open(fpath, encoding=encoding) # noqa: SIM115 35 | content = f.read().split('\n', 1)[1] 36 | f.close() 37 | return content 38 | 39 | def get_(self, url: str, encoding: str = 'utf-8') -> str | None: # noqa: D102 40 | key = self.generate_unique_key_from_url(url) 41 | fpath = os.path.join(self.cache_dir, key) 42 | try: 43 | is_file = stat.S_ISREG(os.stat(fpath).st_mode) 44 | except (FileNotFoundError, OSError): # pragma: no cover 45 | return None 46 | if is_file: # pragma: no branch 47 | creation_time = self.get_creation_time_from_fpath(fpath) 48 | if time.time() < creation_time + self.expiration_seconds: 49 | return self.read_file(fpath, encoding=encoding) 50 | os.remove(fpath) 51 | return None 52 | 53 | def set_(self, url: str, value: str, encoding: str = 'utf-8') -> None: # noqa: D102 54 | key = self.generate_unique_key_from_url(url) 55 | fpath = os.path.join(self.cache_dir, key) 56 | with open(fpath, 'wb') as fp: 57 | now = f'{int(time.time())}\n' 58 | fp.write(now.encode(encoding)) 59 | fp.write(value.encode(encoding)) 60 | 61 | def clean(self) -> None: 62 | """Clean expired entries from the cache.""" 63 | for fname in os.listdir(self.cache_dir): 64 | if fname == '.gitignore': 65 | continue 66 | fpath = os.path.join(self.cache_dir, fname) 67 | creation_time = self.get_creation_time_from_fpath(fpath) 68 | if time.time() > creation_time + self.expiration_seconds: 69 | os.remove(fpath) 70 | 71 | 72 | def get_cache_directory(cache_dir: str) -> str | None: 73 | """Get cache directory.""" 74 | if cache_dir: 75 | return cache_dir 76 | 77 | if not is_platformdirs_installed(): 78 | return None 79 | 80 | try: 81 | from platformdirs import user_data_dir 82 | except ImportError: # pragma: no cover 83 | return None 84 | else: 85 | return user_data_dir('mkdocs-include-markdown-plugin') 86 | 87 | 88 | def initialize_cache(expiration_seconds: int, cache_dir: str) -> Cache | None: 89 | """Initialize a cache instance.""" 90 | cache_directory = get_cache_directory(cache_dir) 91 | 92 | if cache_directory is None: 93 | return None 94 | 95 | os.makedirs(cache_directory, exist_ok=True) 96 | 97 | # Add a `.gitignore` file to prevent the cache directory from being 98 | # included in the repository. This is needed because the cache directory 99 | # can be configured as a relative path with `cache_dir` setting. 100 | gitignore = os.path.join(cache_directory, '.gitignore') 101 | if not os.path.exists(gitignore): 102 | with open(gitignore, 'wb') as f: 103 | f.write(b'*\n') 104 | 105 | cache = Cache(cache_directory, expiration_seconds) 106 | cache.clean() 107 | return cache 108 | 109 | 110 | def is_platformdirs_installed() -> bool: 111 | """Check if `platformdirs` package is installed without importing it.""" 112 | return find_spec('platformdirs') is not None 113 | -------------------------------------------------------------------------------- /src/mkdocs_include_markdown_plugin/config.py: -------------------------------------------------------------------------------- 1 | """Plugin configuration.""" 2 | 3 | from __future__ import annotations 4 | 5 | from mkdocs.config.base import Config 6 | from mkdocs.config.config_options import ( 7 | ListOfItems, 8 | Optional, 9 | Type as MkType, 10 | ) 11 | 12 | 13 | class PluginConfig(Config): # noqa: D101 14 | opening_tag = MkType(str, default='{%') 15 | closing_tag = MkType(str, default='%}') 16 | encoding = MkType(str, default='utf-8') 17 | preserve_includer_indent = MkType(bool, default=True) 18 | dedent = MkType(bool, default=False) 19 | trailing_newlines = MkType(bool, default=True) 20 | comments = MkType(bool, default=False) 21 | rewrite_relative_urls = MkType(bool, default=True) 22 | heading_offset = MkType(int, default=0) 23 | start = Optional(MkType(str)) 24 | end = Optional(MkType(str)) 25 | exclude = ListOfItems(MkType(str), default=[]) 26 | cache = MkType(int, default=0) 27 | cache_dir = MkType(str, default='') 28 | recursive = MkType(bool, default=True) 29 | directives = MkType( 30 | dict, 31 | default={ 32 | '__default': '', 33 | 'include': 'include', 34 | 'include-markdown': 'include-markdown', 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /src/mkdocs_include_markdown_plugin/directive.py: -------------------------------------------------------------------------------- 1 | """Utilities related with the syntax of directives.""" 2 | 3 | from __future__ import annotations 4 | 5 | import functools 6 | import os 7 | import re 8 | import stat 9 | import string 10 | from dataclasses import dataclass 11 | from typing import TYPE_CHECKING 12 | 13 | from mkdocs.exceptions import PluginError 14 | from wcmatch import glob 15 | 16 | from mkdocs_include_markdown_plugin import process 17 | from mkdocs_include_markdown_plugin.logger import logger 18 | 19 | 20 | @dataclass 21 | class DirectiveBoolArgument: # noqa: D101 22 | value: bool 23 | regex: Callable[[], re.Pattern[str]] 24 | 25 | 26 | if TYPE_CHECKING: # pragma: no cover 27 | from collections.abc import Iterable 28 | from typing import Callable, Literal, TypedDict 29 | 30 | DirectiveBoolArgumentsDict = dict[str, DirectiveBoolArgument] 31 | 32 | DefaultValues = TypedDict( 33 | 'DefaultValues', { 34 | 'encoding': str, 35 | 'preserve-includer-indent': bool, 36 | 'dedent': bool, 37 | 'trailing-newlines': bool, 38 | 'comments': bool, 39 | 'rewrite-relative-urls': bool, 40 | 'heading-offset': int, 41 | 'recursive': bool, 42 | 'start': str | None, 43 | 'end': str | None, 44 | }, 45 | ) 46 | 47 | 48 | GLOB_FLAGS = glob.NEGATE | glob.EXTGLOB | glob.GLOBSTAR | glob.BRACE 49 | RE_ESCAPED_PUNCTUATION = re.escape(string.punctuation) 50 | 51 | DOUBLE_QUOTED_STR_RE = r'([^"]|(?<=\\)")+' 52 | SINGLE_QUOTED_STR_RE = r"([^']|(?<=\\)')+" 53 | 54 | # In the following regular expression, the substrings "\{%", "%\}" 55 | # will be replaced by custom opening and closing tags in the `on_config` 56 | # plugin event if required. 57 | INCLUDE_TAG_RE = r''' 58 | (?P<_includer_indent>[ \t\w\\.]*?)\{% 59 | \s* 60 | include 61 | \s+ 62 | (?:"(?P''' + DOUBLE_QUOTED_STR_RE + r''')")?(?:'(?P''' + SINGLE_QUOTED_STR_RE + r''')')? 63 | (?P.*?) 64 | \s* 65 | %\} 66 | ''' # noqa: E501 67 | 68 | TRUE_FALSE_STR_BOOL = { 69 | 'true': True, 70 | 'false': False, 71 | } 72 | 73 | TRUE_FALSE_BOOL_STR = { 74 | True: 'true', 75 | False: 'false', 76 | } 77 | 78 | 79 | @functools.lru_cache 80 | def arg(arg: str) -> re.Pattern[str]: 81 | """Return a compiled regexp to match a boolean argument.""" 82 | return re.compile(rf'{arg}=([{RE_ESCAPED_PUNCTUATION}\w]*)') 83 | 84 | 85 | @functools.lru_cache 86 | def str_arg(arg: str) -> re.Pattern[str]: 87 | """Return a compiled regexp to match a string argument.""" 88 | return re.compile( 89 | rf'{arg}=(?:"({DOUBLE_QUOTED_STR_RE})")?' 90 | rf"(?:'({SINGLE_QUOTED_STR_RE})')?", 91 | ) 92 | 93 | 94 | ARGUMENT_REGEXES = { 95 | 'start': functools.partial(str_arg, 'start'), 96 | 'end': functools.partial(str_arg, 'end'), 97 | 'exclude': functools.partial(str_arg, 'exclude'), 98 | 'encoding': functools.partial(str_arg, 'encoding'), 99 | 100 | # bool 101 | 'comments': functools.partial(arg, 'comments'), 102 | 'preserve-includer-indent': functools.partial( 103 | arg, 'preserve-includer-indent', 104 | ), 105 | 'dedent': functools.partial(arg, 'dedent'), 106 | 'trailing-newlines': functools.partial(arg, 'trailing-newlines'), 107 | 'rewrite-relative-urls': functools.partial(arg, 'rewrite-relative-urls'), 108 | 'recursive': functools.partial(arg, 'recursive'), 109 | 110 | # int 111 | 'heading-offset': functools.partial(arg, 'heading-offset'), 112 | } 113 | 114 | INCLUDE_MARKDOWN_DIRECTIVE_ARGS = set(ARGUMENT_REGEXES) 115 | INCLUDE_DIRECTIVE_ARGS = { 116 | key for key in ARGUMENT_REGEXES if key not in ( 117 | 'rewrite-relative-urls', 'heading-offset', 'comments', 118 | ) 119 | } 120 | 121 | WARN_INVALID_DIRECTIVE_ARGS_REGEX = re.compile( 122 | rf'[\w-]*=[{RE_ESCAPED_PUNCTUATION}\w]*', 123 | ) 124 | 125 | 126 | def _maybe_arguments_iter(arguments_string: str) -> Iterable[str]: 127 | """Iterate over parts of the string that look like arguments.""" 128 | current_string_opening = '' # can be either `'` or `"` 129 | inside_string = False 130 | escaping = False 131 | opening_argument = False # whether we are at the beginning of an argument 132 | current_value = '' 133 | 134 | for c in arguments_string: 135 | if inside_string: 136 | if c == '\\': 137 | escaping = not escaping 138 | continue 139 | elif c == current_string_opening and not escaping: 140 | inside_string = False 141 | current_string_opening = '' 142 | else: 143 | escaping = False 144 | elif c == '=': 145 | new_current_value = '' 146 | for ch in reversed(current_value): 147 | if ch in string.whitespace: 148 | current_value = new_current_value[::-1] 149 | break 150 | new_current_value += ch 151 | yield current_value 152 | current_value = '' 153 | opening_argument = True 154 | elif opening_argument: 155 | opening_argument = False 156 | if c in ('"', "'"): 157 | current_string_opening = c 158 | inside_string = True 159 | current_value += c 160 | current_value += c 161 | else: 162 | current_value += c 163 | 164 | 165 | def warn_invalid_directive_arguments( 166 | arguments_string: str, 167 | directive_lineno: Callable[[], int], 168 | directive: Literal['include', 'include-markdown'], 169 | page_src_path: str | None, 170 | docs_dir: str, 171 | ) -> list[str]: 172 | """Warns about the invalid arguments passed to a directive.""" 173 | used_arguments = [] 174 | valid_args = ( 175 | INCLUDE_DIRECTIVE_ARGS 176 | if directive == 'include' 177 | else INCLUDE_MARKDOWN_DIRECTIVE_ARGS 178 | ) 179 | for maybe_arg in _maybe_arguments_iter(arguments_string): 180 | if maybe_arg not in valid_args: 181 | location = process.file_lineno_message( 182 | page_src_path, docs_dir, directive_lineno(), 183 | ) 184 | logger.warning( 185 | f"Invalid argument '{maybe_arg}' in" 186 | f" '{directive}' directive at {location}. Ignoring...", 187 | ) 188 | else: 189 | used_arguments.append(maybe_arg) 190 | return used_arguments 191 | 192 | 193 | def parse_filename_argument( 194 | match: re.Match[str], 195 | ) -> tuple[str | None, str | None]: 196 | """Return filename argument matched by ``match``.""" 197 | raw_filename = match['double_quoted_filename'] 198 | if raw_filename is None: 199 | raw_filename = match['single_quoted_filename'] 200 | if raw_filename is None: 201 | filename = None 202 | else: 203 | filename = raw_filename.replace(r"\'", "'") 204 | else: 205 | filename = raw_filename.replace(r'\"', '"') 206 | return filename, raw_filename 207 | 208 | 209 | def parse_string_argument(match: re.Match[str] | None) -> str | None: 210 | """Return the string argument matched by ``match``.""" 211 | if match is None: 212 | return None 213 | value = match[1] 214 | if value is None: 215 | value = match[3] 216 | if value is not None: 217 | value = value.replace(r"\'", "'") 218 | else: 219 | value = value.replace(r'\"', '"') 220 | return value 221 | 222 | 223 | def create_include_tag( 224 | opening_tag: str, closing_tag: str, tag: str, 225 | ) -> re.Pattern[str]: 226 | """Create a regex pattern to match an inclusion tag directive. 227 | 228 | Replaces the substrings '$OPENING_TAG' and '$CLOSING_TAG' from 229 | INCLUDE_TAG_RE by the effective tag. 230 | """ 231 | pattern = INCLUDE_TAG_RE 232 | if tag != 'include': 233 | pattern = pattern.replace( 234 | ' include', 235 | ( 236 | ' include-markdown' if tag == 'include-markdown' 237 | else f' {re.escape(tag)}' 238 | ), 239 | 1, 240 | ) 241 | 242 | if opening_tag != '{%': 243 | pattern = pattern.replace(r'\{%', re.escape(opening_tag), 1) 244 | 245 | if closing_tag != '%}': 246 | pattern = pattern.replace(r'%\}', re.escape(closing_tag), 1) 247 | 248 | return re.compile(pattern, flags=re.VERBOSE | re.DOTALL) 249 | 250 | 251 | def parse_bool_options( 252 | option_names: list[str], 253 | defaults: DefaultValues, 254 | arguments_string: str, 255 | used_arguments: list[str], 256 | ) -> tuple[DirectiveBoolArgumentsDict, list[str]]: 257 | """Parse boolean options from arguments string.""" 258 | invalid_args: list[str] = [] 259 | 260 | bool_options: dict[str, DirectiveBoolArgument] = {} 261 | for option_name in option_names: 262 | bool_options[option_name] = DirectiveBoolArgument( 263 | value=defaults[option_name], # type: ignore 264 | regex=ARGUMENT_REGEXES[option_name], 265 | ) 266 | 267 | for arg_name, arg in bool_options.items(): 268 | if arg_name not in used_arguments: 269 | continue 270 | bool_arg_match = arg.regex().search(arguments_string) 271 | try: 272 | bool_options[arg_name].value = TRUE_FALSE_STR_BOOL[ 273 | (bool_arg_match and bool_arg_match[1]) 274 | or TRUE_FALSE_BOOL_STR[arg.value] 275 | ] 276 | except KeyError: 277 | invalid_args.append(arg_name) 278 | return bool_options, invalid_args 279 | 280 | 281 | def resolve_file_paths_to_include( # noqa: PLR0912 282 | include_string: str, 283 | includer_page_src_path: str | None, 284 | docs_dir: str, 285 | ignore_paths: list[str], 286 | ) -> tuple[list[str], bool]: 287 | """Resolve the file paths to include for a directive.""" 288 | if process.is_url(include_string): 289 | return [include_string], True 290 | 291 | if process.is_absolute_path(include_string): 292 | if os.name == 'nt': # pragma: no cover 293 | # Windows 294 | fpath = os.path.normpath(include_string) 295 | try: 296 | is_file = stat.S_ISREG(os.stat(fpath).st_mode) 297 | except (FileNotFoundError, OSError): 298 | is_file = False 299 | if not is_file: 300 | return [], False 301 | 302 | return process.filter_paths( 303 | [fpath], ignore_paths, 304 | ), False 305 | 306 | try: 307 | is_file = stat.S_ISREG(os.stat(include_string).st_mode) 308 | except (FileNotFoundError, OSError): 309 | is_file = False 310 | return process.filter_paths( 311 | [include_string] if is_file else glob.iglob( 312 | include_string, flags=GLOB_FLAGS, 313 | ), 314 | ignore_paths), False 315 | 316 | if process.is_relative_path(include_string): 317 | if includer_page_src_path is None: # pragma: no cover 318 | raise PluginError( 319 | 'Relative paths are not allowed when the includer page' 320 | ' source path is not provided. The include string' 321 | f" '{include_string}' is located inside a generated page.", 322 | ) 323 | root_dir = os.path.abspath( 324 | os.path.dirname(includer_page_src_path), 325 | ) 326 | paths = [] 327 | include_path = os.path.join(root_dir, include_string) 328 | try: 329 | is_file = stat.S_ISREG(os.stat(include_path).st_mode) 330 | except (FileNotFoundError, OSError): 331 | is_file = False 332 | if is_file: 333 | paths.append(include_path) 334 | else: 335 | for fp in glob.iglob( 336 | include_string, 337 | flags=GLOB_FLAGS, 338 | root_dir=root_dir, 339 | ): 340 | paths.append(os.path.join(root_dir, fp)) 341 | return process.filter_paths(paths, ignore_paths), False 342 | 343 | # relative to docs_dir 344 | paths = [] 345 | root_dir = docs_dir 346 | include_path = os.path.join(root_dir, include_string) 347 | try: 348 | is_file = stat.S_ISREG(os.stat(include_path).st_mode) 349 | except (FileNotFoundError, OSError): 350 | is_file = False 351 | if is_file: 352 | paths.append(include_path) 353 | else: 354 | for fp in glob.iglob( 355 | include_string, 356 | flags=GLOB_FLAGS, 357 | root_dir=root_dir, 358 | ): 359 | paths.append(os.path.join(root_dir, fp)) 360 | return process.filter_paths(paths, ignore_paths), False 361 | 362 | 363 | def resolve_file_paths_to_exclude( 364 | exclude_string: str, 365 | includer_page_src_path: str | None, 366 | docs_dir: str, 367 | ) -> list[str]: 368 | """Resolve the file paths to exclude for a directive.""" 369 | if process.is_absolute_path(exclude_string): 370 | return glob.glob(exclude_string, flags=GLOB_FLAGS) 371 | 372 | if process.is_relative_path(exclude_string): 373 | if includer_page_src_path is None: # pragma: no cover 374 | raise PluginError( 375 | 'Relative paths are not allowed when the includer page' 376 | ' source path is not provided. The exclude string' 377 | f" '{exclude_string}' is located inside a generated page.", 378 | ) 379 | root_dir = os.path.abspath( 380 | os.path.dirname(includer_page_src_path), 381 | ) 382 | return [ 383 | os.path.normpath( 384 | os.path.join(root_dir, fp), 385 | ) for fp in glob.glob( 386 | exclude_string, 387 | flags=GLOB_FLAGS, 388 | root_dir=root_dir, 389 | ) 390 | ] 391 | 392 | return glob.glob( # pragma: no cover 393 | exclude_string, 394 | flags=GLOB_FLAGS, 395 | root_dir=docs_dir, 396 | ) 397 | -------------------------------------------------------------------------------- /src/mkdocs_include_markdown_plugin/files_watcher.py: -------------------------------------------------------------------------------- 1 | """Implementation to watch for files when using livereload server.""" 2 | 3 | from __future__ import annotations 4 | 5 | 6 | class FilesWatcher: # noqa: D101 7 | def __init__(self) -> None: # noqa: D107 pragma: no cover 8 | self.prev_included_files: list[str] = [] 9 | self.included_files: list[str] = [] 10 | -------------------------------------------------------------------------------- /src/mkdocs_include_markdown_plugin/logger.py: -------------------------------------------------------------------------------- 1 | """Mkdocs plugin logger.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | 7 | 8 | # TODO: when Mkdocs < 1.5.0 support is dropped, use 9 | # mkdocs.plugin.get_plugin_logger 10 | logger = logging.getLogger('mkdocs.plugins.include_markdown') 11 | -------------------------------------------------------------------------------- /src/mkdocs_include_markdown_plugin/plugin.py: -------------------------------------------------------------------------------- 1 | """Plugin entry point.""" 2 | 3 | from __future__ import annotations 4 | 5 | from collections.abc import Callable 6 | from functools import cached_property 7 | from typing import TYPE_CHECKING 8 | 9 | from mkdocs.exceptions import PluginError 10 | from mkdocs.plugins import BasePlugin, event_priority 11 | 12 | 13 | if TYPE_CHECKING: # pragma: no cover 14 | 15 | from mkdocs.config.defaults import MkDocsConfig 16 | from mkdocs.livereload import LiveReloadServer 17 | from mkdocs.structure.files import Files 18 | from mkdocs.structure.pages import Page 19 | 20 | from mkdocs_include_markdown_plugin.cache import Cache, initialize_cache 21 | from mkdocs_include_markdown_plugin.config import PluginConfig 22 | from mkdocs_include_markdown_plugin.event import ( 23 | on_page_markdown as _on_page_markdown, 24 | ) 25 | from mkdocs_include_markdown_plugin.files_watcher import FilesWatcher 26 | 27 | 28 | class IncludeMarkdownPlugin(BasePlugin[PluginConfig]): 29 | _cache: Cache | None = None 30 | _server: LiveReloadServer | None = None 31 | 32 | def on_config(self, config: MkDocsConfig) -> MkDocsConfig: 33 | if self.config.cache > 0: 34 | cache = initialize_cache(self.config.cache, self.config.cache_dir) 35 | if cache is None: 36 | raise PluginError( 37 | 'Either `cache_dir` global setting must be configured or' 38 | ' `platformdirs` package is required to use the' 39 | ' `cache` option. Install mkdocs-include-markdown-plugin' 40 | " with the 'cache' extra to install `platformdirs`.", 41 | ) 42 | self._cache = cache 43 | 44 | if '__default' not in self.config.directives: # pragma: no cover 45 | for directive in self.config.directives: 46 | if directive not in ('include', 'include-markdown'): 47 | raise PluginError( 48 | f"Invalid directive name '{directive}' at 'directives'" 49 | ' global setting. Valid values are "include" and' 50 | ' "include-markdown".', 51 | ) 52 | 53 | return config 54 | 55 | @cached_property 56 | def _files_watcher(self) -> FilesWatcher: 57 | return FilesWatcher() 58 | 59 | def _update_watched_files(self) -> None: # pragma: no cover 60 | """Function executed on server reload. 61 | 62 | At this execution point, the ``self._server`` attribute must be set. 63 | """ 64 | watcher, server = self._files_watcher, self._server 65 | 66 | # unwatch previous watched files not needed anymore 67 | for file_path in watcher.prev_included_files: 68 | if file_path not in watcher.included_files: 69 | server.unwatch(file_path) # type: ignore 70 | watcher.prev_included_files = watcher.included_files[:] 71 | 72 | # watch new included files 73 | for file_path in watcher.included_files: 74 | server.watch(file_path, recursive=False) # type: ignore 75 | watcher.included_files = [] 76 | 77 | def on_page_content( 78 | self, 79 | html: str, 80 | page: Page, # noqa: ARG002 81 | config: MkDocsConfig, # noqa: ARG002 82 | files: Files, # noqa: ARG002 83 | ) -> str: 84 | if self._server is not None: # pragma: no cover 85 | self._update_watched_files() 86 | return html 87 | 88 | def on_serve( 89 | self, 90 | server: LiveReloadServer, 91 | config: MkDocsConfig, # noqa: ARG002 92 | builder: Callable, # noqa: ARG002 93 | ) -> None: 94 | if self._server is None: # pragma: no cover 95 | self._server = server 96 | self._update_watched_files() 97 | 98 | @event_priority(100) 99 | def on_page_markdown( 100 | self, 101 | markdown: str, 102 | page: Page, 103 | config: MkDocsConfig, 104 | files: Files, # noqa: ARG002 105 | ) -> str: 106 | return _on_page_markdown( 107 | markdown, 108 | page, 109 | config.docs_dir, 110 | plugin=self, 111 | ) 112 | -------------------------------------------------------------------------------- /src/mkdocs_include_markdown_plugin/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/aa17cc062d5a63caec5f43c0425974c6a34725be/src/mkdocs_include_markdown_plugin/py.typed -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/aa17cc062d5a63caec5f43c0425974c6a34725be/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import pytest 5 | 6 | 7 | TESTS_DIR = os.path.abspath(os.path.dirname(__file__)) 8 | SRC_DIR = os.path.abspath(os.path.join(TESTS_DIR, '..', 'src')) 9 | for d in (SRC_DIR, TESTS_DIR): 10 | if d not in sys.path: 11 | sys.path.insert(0, d) 12 | 13 | from mkdocs_include_markdown_plugin import IncludeMarkdownPlugin # noqa: E402 14 | 15 | 16 | @pytest.fixture 17 | def page(): 18 | """Fake mkdocs page object.""" 19 | def _page(file_path): 20 | return type( 21 | 'FakeMkdocsPage', (), { 22 | 'file': type( 23 | 'FakeMdocsPageFile', (), { 24 | 'abs_src_path': file_path, 25 | }, 26 | ), 27 | }, 28 | ) 29 | return _page 30 | 31 | 32 | @pytest.fixture 33 | def plugin(request): 34 | """Populate a plugin, with optional indirect config parameter.""" 35 | plugin = IncludeMarkdownPlugin() 36 | errors, warnings = plugin.load_config(getattr(request, 'param', {})) 37 | assert errors == [] 38 | assert warnings == [] 39 | return plugin 40 | -------------------------------------------------------------------------------- /tests/test_integration/test_cache_integration.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass 3 | 4 | import pytest 5 | from mkdocs.exceptions import PluginError 6 | 7 | import mkdocs_include_markdown_plugin.cache 8 | from mkdocs_include_markdown_plugin import IncludeMarkdownPlugin 9 | from mkdocs_include_markdown_plugin.cache import ( 10 | Cache, 11 | get_cache_directory, 12 | initialize_cache, 13 | is_platformdirs_installed, 14 | ) 15 | from mkdocs_include_markdown_plugin.event import on_page_markdown 16 | from testing_helpers import parametrize_directives 17 | 18 | 19 | @pytest.mark.parametrize( 20 | ('url', 'expected_result'), 21 | ( 22 | ( 23 | 'https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/master/examples/basic/mkdocs.yml', # noqa: E501 24 | '''site_name: Foo 25 | plugins: 26 | - include-markdown 27 | ''', 28 | ), 29 | ( 30 | 'https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/master/examples/basic/docs/included.md', # noqa: E501 31 | '''Some ignored content. 32 | 33 | <--start--> 34 | 35 | Some included content. 36 | ''', 37 | ), 38 | ), 39 | ) 40 | @parametrize_directives 41 | def test_page_included_by_url_is_cached( 42 | directive, 43 | url, 44 | expected_result, 45 | page, 46 | tmp_path, 47 | plugin, 48 | ): 49 | if not is_platformdirs_installed(): 50 | assert initialize_cache(600, '') is None 51 | return 52 | 53 | cache_dir = get_cache_directory('') 54 | os.makedirs(cache_dir, exist_ok=True) 55 | 56 | file_path = os.path.join( 57 | cache_dir, Cache.generate_unique_key_from_url(url), 58 | ) 59 | if os.path.isfile(file_path): 60 | os.remove(file_path) 61 | 62 | cache = Cache(cache_dir, 600) 63 | 64 | def run(): 65 | return on_page_markdown( 66 | f'''{{% 67 | {directive} "{url}" 68 | %}}''', 69 | page(tmp_path / 'includer.md'), 70 | tmp_path, 71 | plugin, 72 | http_cache=cache, 73 | ) 74 | 75 | assert run() == expected_result 76 | 77 | assert os.path.isfile(file_path) 78 | run() 79 | assert os.path.isfile(file_path) 80 | 81 | os.remove(file_path) 82 | 83 | 84 | def test_cache_setting_when_not_available_raises_error(monkeypatch): 85 | @dataclass 86 | class FakeConfig: 87 | cache: int 88 | cache_dir: str 89 | directives: dict[str, str] 90 | 91 | monkeypatch.setattr( 92 | mkdocs_include_markdown_plugin.cache, 93 | 'is_platformdirs_installed', 94 | lambda: False, 95 | ) 96 | plugin = IncludeMarkdownPlugin() 97 | plugin.config = FakeConfig( 98 | cache=600, cache_dir='', directives={'__default': ''}, 99 | ) 100 | with pytest.raises(PluginError) as exc: 101 | plugin.on_config({}) 102 | assert ( 103 | 'Either `cache_dir` global setting must be configured or' 104 | ' `platformdirs` package is required' 105 | ) in str(exc.value) 106 | 107 | 108 | def test_cache_setting_available_with_cache_dir(monkeypatch): 109 | @dataclass 110 | class FakeConfig: 111 | cache: int 112 | cache_dir: str 113 | directives: dict[str, str] 114 | 115 | monkeypatch.setattr( 116 | mkdocs_include_markdown_plugin.cache, 117 | 'is_platformdirs_installed', 118 | lambda: False, 119 | ) 120 | plugin = IncludeMarkdownPlugin() 121 | plugin.config = FakeConfig( 122 | cache=600, cache_dir='foo', directives={'__default': ''}, 123 | ) 124 | plugin.on_config({}) 125 | -------------------------------------------------------------------------------- /tests/test_integration/test_examples.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | 5 | import pytest 6 | from mkdocs import config 7 | from mkdocs.commands.build import build 8 | from mkdocs.exceptions import Abort 9 | 10 | from mkdocs_include_markdown_plugin.cache import is_platformdirs_installed 11 | from testing_helpers import rootdir 12 | 13 | 14 | EXAMPLES_DIR = os.path.join(rootdir, 'examples') 15 | 16 | 17 | def config_is_using_cache_setting(config_file_path): 18 | with open(config_file_path, encoding='utf-8') as f: 19 | return 'cache:' in f.read() 20 | 21 | 22 | @pytest.mark.parametrize('dirname', os.listdir(EXAMPLES_DIR)) 23 | def test_examples_subprocess(dirname): 24 | example_dir = os.path.join(EXAMPLES_DIR, dirname) 25 | config_file = os.path.join(example_dir, 'mkdocs.yml') 26 | expected_returncode = 1 if config_is_using_cache_setting( 27 | config_file, 28 | ) and not is_platformdirs_installed() else 0 29 | 30 | proc = subprocess.Popen( 31 | [sys.executable, '-mmkdocs', 'build'], 32 | cwd=example_dir, 33 | stdout=subprocess.PIPE, 34 | stderr=subprocess.PIPE, 35 | ) 36 | stdout, stderr = proc.communicate() 37 | 38 | assert proc.returncode == expected_returncode, ( 39 | f'{stdout.decode("utf-8")}\n{stderr.decode("utf-8")}' 40 | ) 41 | 42 | 43 | @pytest.mark.parametrize('dirname', os.listdir(EXAMPLES_DIR)) 44 | def test_examples_api(dirname): 45 | example_dir = os.path.join(EXAMPLES_DIR, dirname) 46 | config_file = os.path.join(example_dir, 'mkdocs.yml') 47 | expected_to_raise_exc = ( 48 | config_is_using_cache_setting(config_file) and 49 | not is_platformdirs_installed() 50 | ) 51 | 52 | def run(): 53 | cfg = config.load_config(config_file=config_file) 54 | build(cfg, dirty=False) 55 | 56 | if expected_to_raise_exc: 57 | with pytest.raises(Abort): 58 | run() 59 | else: 60 | run() 61 | -------------------------------------------------------------------------------- /tests/test_unit/test_arguments.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import os 3 | import re 4 | 5 | import pytest 6 | from mkdocs.exceptions import PluginError 7 | 8 | from mkdocs_include_markdown_plugin.event import on_page_markdown 9 | from testing_helpers import parametrize_directives, unix_only 10 | 11 | 12 | @pytest.mark.parametrize( 13 | ('directive', 'arguments'), 14 | ( 15 | pytest.param( 16 | 'include', 17 | [ 18 | 'preserve-includer-indent', 19 | 'dedent', 20 | 'trailing-newlines', 21 | ], 22 | id='include', 23 | ), 24 | pytest.param( 25 | 'include-markdown', 26 | [ 27 | 'preserve-includer-indent', 28 | 'dedent', 29 | 'rewrite-relative-urls', 30 | 'comments', 31 | 'trailing-newlines', 32 | ], 33 | id='include-markdown', 34 | ), 35 | ), 36 | ) 37 | def test_invalid_bool_arguments( 38 | directive, 39 | arguments, 40 | page, 41 | tmp_path, 42 | plugin, 43 | caplog, 44 | ): 45 | for argument_name in arguments: 46 | page_to_include_filepath = tmp_path / 'included.md' 47 | page_to_include_filepath.write_text('Included\n') 48 | 49 | filename = 'includer.md' 50 | 51 | with pytest.raises(PluginError) as exc: 52 | on_page_markdown( 53 | f'''{{% 54 | {directive} "{page_to_include_filepath}" 55 | {argument_name}=invalidoption 56 | %}}''', 57 | page(tmp_path / filename), 58 | tmp_path, 59 | plugin, 60 | ) 61 | assert str(exc.value) == ( 62 | f"Invalid value for '{argument_name}' argument of '{directive}'" 63 | f' directive at {filename}:1. Possible values are true or false.' 64 | ) 65 | assert len(caplog.records) == 0 66 | 67 | 68 | @parametrize_directives 69 | def test_start_end_mixed_quotes(directive, page, caplog, tmp_path, plugin): 70 | page_to_include_filepath = tmp_path / 'included.md' 71 | page_to_include_filepath.write_text('''Content that should be ignored 72 | 73 | Content to include 74 | 75 | More content that should be ignored 76 | ''') 77 | 78 | includer_file_content = f'''{{% 79 | {directive} "{page_to_include_filepath}" 80 | start='' 81 | end="" 82 | %}}''' 83 | result = on_page_markdown( 84 | includer_file_content, 85 | page(tmp_path / 'includer.md'), 86 | tmp_path, 87 | plugin, 88 | ) 89 | assert result == '\nContent to include\n' 90 | 91 | assert caplog.records == [] 92 | 93 | 94 | @pytest.mark.parametrize('argument', ('start', 'end')) 95 | @parametrize_directives 96 | def test_invalid_start_end_arguments( 97 | argument, 98 | directive, 99 | page, 100 | caplog, 101 | tmp_path, 102 | plugin, 103 | ): 104 | page_to_include_filepath = tmp_path / 'included.md' 105 | included_content = '''Content that should be ignored 106 | 107 | Content to include 108 | 109 | More content that should be ignored 110 | ''' 111 | page_to_include_filepath.write_text(included_content) 112 | 113 | includer_file_content = f''' 114 | {{% 115 | {directive} "{page_to_include_filepath}" 116 | {argument}='' 117 | %}}''' 118 | with pytest.raises(PluginError) as exc: 119 | on_page_markdown( 120 | includer_file_content, 121 | page(tmp_path / 'includer.md'), 122 | tmp_path, 123 | plugin, 124 | ) 125 | assert str(exc.value) == ( 126 | f"Invalid empty '{argument}' argument in '{directive}'" 127 | ' directive at includer.md:2' 128 | ) 129 | 130 | assert len([record.msg for record in caplog.records]) == 0 131 | 132 | 133 | @unix_only 134 | @parametrize_directives 135 | def test_exclude_double_quote_escapes( 136 | directive, page, tmp_path, plugin, caplog, 137 | ): 138 | drectory_to_include = tmp_path / 'exclude_double_quote_escapes' 139 | drectory_to_include.mkdir() 140 | 141 | page_to_include_filepath = drectory_to_include / 'included.md' 142 | page_to_include_filepath.write_text('Content that should be included\n') 143 | 144 | page_to_exclude_filepath = drectory_to_include / 'igno"re"d.md' 145 | page_to_exclude_filepath.write_text('Content that should be excluded\n') 146 | page_to_exclude_escaped_filepath = str( 147 | page_to_exclude_filepath, 148 | ).replace('"', '\\"') 149 | 150 | includer_glob = os.path.join(str(drectory_to_include), '*.md') 151 | includer_file_content = f'''{{% 152 | {directive} "{includer_glob}" 153 | exclude="{page_to_exclude_escaped_filepath}" 154 | %}}''' 155 | result = on_page_markdown( 156 | includer_file_content, 157 | page(tmp_path / 'includer.md'), 158 | tmp_path, 159 | plugin, 160 | ) 161 | assert result == 'Content that should be included\n' 162 | assert caplog.records == [] 163 | 164 | 165 | @unix_only 166 | @parametrize_directives 167 | def test_invalid_exclude_argument(directive, page, tmp_path, caplog, plugin): 168 | drectory_to_include = tmp_path / 'exclude_double_quote_escapes' 169 | drectory_to_include.mkdir() 170 | 171 | page_to_include_filepath = drectory_to_include / 'included.md' 172 | page_to_include_filepath.write_text('Content that should be included\n') 173 | 174 | page_to_exclude_filepath = drectory_to_include / 'igno"re"d.md' 175 | page_to_exclude_filepath.write_text('Content that should be excluded\n') 176 | 177 | includer_glob = os.path.join(str(drectory_to_include), '*.md') 178 | 179 | includer_file_content = f'''{{% 180 | {directive} "{includer_glob}" 181 | exclude= 182 | %}}''' 183 | 184 | with pytest.raises(PluginError) as exc: 185 | on_page_markdown( 186 | includer_file_content, 187 | page(tmp_path / 'includer.md'), 188 | tmp_path, 189 | plugin, 190 | ) 191 | 192 | assert len(caplog.records) == 0 193 | assert str(exc.value) == ( 194 | f"Invalid empty 'exclude' argument in '{directive}' directive" 195 | ' at includer.md:1' 196 | ) 197 | 198 | 199 | @parametrize_directives 200 | def test_empty_encoding_argument(directive, page, tmp_path, plugin, caplog): 201 | page_to_include_filepath = tmp_path / 'included.md' 202 | page_to_include_filepath.write_text('Content to include') 203 | 204 | includer_file_content = f'''{{% 205 | {directive} "{page_to_include_filepath}" 206 | encoding= 207 | %}}''' 208 | 209 | with pytest.raises(PluginError) as exc: 210 | on_page_markdown( 211 | includer_file_content, 212 | page(tmp_path / 'includer.md'), 213 | tmp_path, 214 | plugin, 215 | ) 216 | 217 | assert len(caplog.records) == 0 218 | assert str(exc.value) == ( 219 | f"Invalid empty 'encoding' argument in '{directive}'" 220 | ' directive at includer.md:1' 221 | ) 222 | 223 | 224 | @pytest.mark.parametrize( 225 | ('argument_value', 'exception_message'), 226 | ( 227 | pytest.param( 228 | 'invalidoption', ( 229 | "Invalid 'heading-offset' argument 'invalidoption' in" 230 | " 'include-markdown' directive at includer.md:1" 231 | ), 232 | id='invalidoption', 233 | ), 234 | pytest.param( 235 | '', ( 236 | "Invalid empty 'heading-offset' argument in" 237 | " 'include-markdown' directive at includer.md:1" 238 | ), 239 | id='empty', 240 | ), 241 | ), 242 | ) 243 | def test_invalid_heading_offset_arguments( 244 | argument_value, 245 | exception_message, 246 | page, 247 | tmp_path, 248 | plugin, 249 | caplog, 250 | ): 251 | page_to_include_filepath = tmp_path / 'included.md' 252 | page_to_include_filepath.write_text('# Content to include') 253 | 254 | with pytest.raises(PluginError) as exc: 255 | on_page_markdown( 256 | f'''{{% 257 | include-markdown "{page_to_include_filepath}" 258 | heading-offset={argument_value} 259 | %}}''', 260 | page(tmp_path / 'includer.md'), 261 | tmp_path, 262 | plugin, 263 | ) 264 | 265 | assert len(caplog.records) == 0 266 | assert str(exc.value) == exception_message 267 | 268 | 269 | @parametrize_directives 270 | def test_invalid_argument_name(directive, page, tmp_path, plugin, caplog): 271 | page_to_include_filepath = tmp_path / 'included.md' 272 | page_to_include_filepath.write_text('Content to include') 273 | 274 | includer_file_content = f'''{{% 275 | {directive} "{page_to_include_filepath}" 276 | invalid-argument=true 277 | %}}''' 278 | assert on_page_markdown( 279 | includer_file_content, 280 | page(tmp_path / 'includer.md'), 281 | tmp_path, 282 | plugin, 283 | ) == 'Content to include' 284 | 285 | assert len(caplog.records) == 1 286 | assert caplog.records[0].msg == ( 287 | f"Invalid argument 'invalid-argument' in '{directive}'" 288 | " directive at includer.md:1. Ignoring..." 289 | ) 290 | 291 | 292 | class TestFilename: 293 | double_quoted_filenames = [ 294 | 'inc"luded.md', 'inc"lude"d.md', 'included.md"', '"included.md', 295 | ] 296 | single_quoted_filenames = [ 297 | fname.replace('"', "'") for fname in double_quoted_filenames 298 | ] 299 | 300 | @unix_only 301 | @parametrize_directives 302 | @pytest.mark.parametrize('filename', double_quoted_filenames) 303 | def test_not_escaped_double_quotes( 304 | self, directive, filename, page, tmp_path, plugin, caplog, 305 | ): 306 | page_to_include_filepath = tmp_path / filename 307 | page_to_include_filepath.write_text('Foo\n') 308 | 309 | with pytest.raises(PluginError) as exc: 310 | on_page_markdown( 311 | f'{{% {directive} "{page_to_include_filepath}" %}}', 312 | page(tmp_path / 'includer.md'), 313 | tmp_path, 314 | plugin, 315 | ) 316 | 317 | assert len(caplog.records) == 0 318 | assert re.match( 319 | r'^No files found including ', 320 | str(exc.value), 321 | ) 322 | 323 | @unix_only 324 | @parametrize_directives 325 | @pytest.mark.parametrize('filename', double_quoted_filenames) 326 | def test_escaped_double_quotes( 327 | self, directive, filename, page, tmp_path, plugin, 328 | ): 329 | included_content = 'Foo\n' 330 | page_to_include_filepath = tmp_path / filename 331 | page_to_include_filepath.write_text(included_content) 332 | 333 | # escape filename passed as argument 334 | escaped_page_to_include_filepath = str( 335 | page_to_include_filepath, 336 | ).replace('"', '\\"') 337 | result = on_page_markdown( 338 | f'''{{% 339 | {directive} "{escaped_page_to_include_filepath}" 340 | %}}''', 341 | page(tmp_path / 'includer.md'), 342 | tmp_path, 343 | plugin, 344 | ) 345 | assert result == included_content 346 | 347 | @parametrize_directives 348 | @pytest.mark.parametrize('filename', single_quoted_filenames) 349 | def test_escaped_single_quotes( 350 | self, filename, directive, page, tmp_path, plugin, 351 | ): 352 | included_content = 'Foo\n' 353 | page_to_include_filepath = tmp_path / filename 354 | page_to_include_filepath.write_text(included_content) 355 | 356 | # escape filename passed as argument 357 | escaped_page_to_include_filepath = str( 358 | page_to_include_filepath, 359 | ).replace("'", "\\'") 360 | result = on_page_markdown( 361 | f'''{{% 362 | {directive} '{escaped_page_to_include_filepath}' 363 | %}}''', 364 | page(tmp_path / 'includer.md'), 365 | tmp_path, 366 | plugin, 367 | ) 368 | assert result == included_content 369 | 370 | @unix_only 371 | @parametrize_directives 372 | @pytest.mark.parametrize('filename', double_quoted_filenames) 373 | def test_unescaped_double_quotes( 374 | self, filename, directive, page, tmp_path, plugin, 375 | ): 376 | included_content = 'Foo\n' 377 | page_to_include_filepath = tmp_path / filename 378 | page_to_include_filepath.write_text(included_content) 379 | 380 | result = on_page_markdown( 381 | f'''{{% 382 | {directive} '{page_to_include_filepath}' 383 | %}}''', 384 | page(tmp_path / 'includer.md'), 385 | tmp_path, 386 | plugin, 387 | ) 388 | assert result == included_content 389 | 390 | @parametrize_directives 391 | @pytest.mark.parametrize('filename', single_quoted_filenames) 392 | def test_unescaped_single_quotes( 393 | self, filename, directive, page, tmp_path, plugin, 394 | ): 395 | included_content = 'Foo\n' 396 | page_to_include_filepath = tmp_path / filename 397 | page_to_include_filepath.write_text(included_content) 398 | 399 | result = on_page_markdown( 400 | f'''{{% 401 | {directive} "{page_to_include_filepath}" 402 | %}}''', 403 | page(tmp_path / 'includer.md'), 404 | tmp_path, 405 | plugin, 406 | ) 407 | assert result == included_content 408 | 409 | @unix_only 410 | @parametrize_directives 411 | @pytest.mark.parametrize( 412 | 'filename', ("inc'luded\".md", "''i\"nc\"lude'd.md"), 413 | ) 414 | @pytest.mark.parametrize( 415 | 'quote', ('"', "'"), ids=('quote="', "quote='"), 416 | ) 417 | @pytest.mark.parametrize( 418 | 'escape', (True, False), ids=('escape=True', 'escape=False'), 419 | ) 420 | def test_mixed_quotes( 421 | self, 422 | filename, 423 | quote, 424 | escape, 425 | directive, 426 | page, 427 | tmp_path, 428 | plugin, 429 | caplog, 430 | ): 431 | included_content = 'Foo\n' 432 | page_to_include_filepath = tmp_path / filename 433 | page_to_include_filepath.write_text(included_content) 434 | 435 | if escape: 436 | escaped_page_to_include_filepath = str( 437 | page_to_include_filepath, 438 | ).replace(quote, f'\\{quote}') 439 | else: 440 | escaped_page_to_include_filepath = page_to_include_filepath 441 | 442 | markdown = f'''{{% 443 | {directive} {quote}{escaped_page_to_include_filepath}{quote} 444 | %}}''' 445 | 446 | func = functools.partial( 447 | on_page_markdown, 448 | markdown, 449 | page(tmp_path / 'includer.md'), 450 | tmp_path, 451 | plugin, 452 | ) 453 | 454 | if escape: 455 | assert func() == included_content 456 | else: 457 | with pytest.raises(PluginError) as exc: 458 | func() 459 | assert re.match( 460 | r'No files found including ', 461 | str(exc.value), 462 | ) 463 | assert len(caplog.records) == 0 464 | 465 | @parametrize_directives 466 | def test_no_filename(self, directive, page, tmp_path, plugin, caplog): 467 | filename = 'includer.md' 468 | 469 | with pytest.raises(PluginError) as exc: 470 | on_page_markdown( 471 | f'\n\n{{% {directive} %}}', 472 | page(tmp_path / filename), 473 | tmp_path, 474 | plugin, 475 | ) 476 | 477 | assert str(exc.value) == ( 478 | f"Found no path passed including with '{directive}' directive" 479 | f' at {filename}:3' 480 | ) 481 | assert len(caplog.records) == 0 482 | 483 | @parametrize_directives 484 | def test_non_existent_filename( 485 | self, 486 | directive, 487 | page, 488 | tmp_path, 489 | plugin, 490 | caplog, 491 | ): 492 | page_content = f'''{{% 493 | {directive} "/path/to/file/that/does/not/exists" 494 | start="" 495 | end="" 496 | %}}''' 497 | 498 | page_filepath = tmp_path / 'example.md' 499 | page_filepath.write_text(page_content) 500 | 501 | with pytest.raises(PluginError) as exc: 502 | on_page_markdown( 503 | page_content, 504 | page(page_filepath), 505 | tmp_path, 506 | plugin, 507 | ) 508 | 509 | assert len(caplog.records) == 0 510 | assert re.match(r'No files found including ', str(exc.value)) 511 | -------------------------------------------------------------------------------- /tests/test_unit/test_cache.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from mkdocs_include_markdown_plugin.cache import ( 5 | Cache, 6 | get_cache_directory, 7 | initialize_cache, 8 | is_platformdirs_installed, 9 | ) 10 | 11 | 12 | def test_cache_read_file(tmp_path): 13 | cache = Cache(tmp_path) 14 | assert cache.read_file('pyproject.toml').split('\n', 1)[0] == ( 15 | 'name = "mkdocs-include-markdown-plugin"' 16 | ) 17 | 18 | 19 | def test_cache_expiration_on_get(tmp_path): 20 | cache = Cache(tmp_path) 21 | cache.set_('foo', f'{time.time() - 600*10}\nbar') 22 | assert cache.get_('foo') is None 23 | 24 | 25 | def test_cache_clean(tmp_path): 26 | now_ts = int(time.time()) 27 | 28 | file1 = tmp_path / 'file1' 29 | file1.write_text(f'{now_ts}\n') 30 | file2 = tmp_path / 'file2' 31 | file2.write_text(f'{now_ts}\n') 32 | 33 | assert len(os.listdir(tmp_path)) == 2 34 | 35 | cache = Cache(tmp_path, 0) 36 | cache.clean() 37 | 38 | assert len(os.listdir(tmp_path)) == 0 39 | 40 | 41 | def test_get_cache_directory_empty(): 42 | if not is_platformdirs_installed(): 43 | assert get_cache_directory('') is None 44 | else: 45 | assert isinstance(get_cache_directory(''), str) 46 | 47 | 48 | def test_get_cache_directory_custom(): 49 | assert get_cache_directory('foo') == 'foo' 50 | 51 | 52 | def test_initialize_cache_not_cache_dir(): 53 | if not is_platformdirs_installed(): 54 | assert initialize_cache(300, '') is None 55 | else: 56 | assert isinstance(initialize_cache(300, ''), Cache) 57 | 58 | 59 | def test_initialize_cache_cache_dir(): 60 | assert isinstance(initialize_cache(300, 'foo'), Cache) 61 | -------------------------------------------------------------------------------- /tests/test_unit/test_config.py: -------------------------------------------------------------------------------- 1 | """``include`` directive tests.""" 2 | 3 | import pytest 4 | 5 | from mkdocs_include_markdown_plugin.event import on_page_markdown 6 | from testing_helpers import unix_only 7 | 8 | 9 | TESTS_ARGUMENTS = ( 10 | 'includer_schema', 11 | 'content_to_include', 12 | 'expected_result', 13 | 'plugin', 14 | ) 15 | 16 | 17 | def _run_test( 18 | includer_schema, 19 | content_to_include, 20 | expected_result, 21 | plugin, 22 | page, 23 | caplog, 24 | tmp_path, 25 | ): 26 | included_file = tmp_path / 'included.md' 27 | includer_file = tmp_path / 'includer.md' 28 | 29 | included_file.write_text(content_to_include) 30 | includer_file.write_text( 31 | content_to_include.replace('{filepath}', included_file.as_posix()), 32 | ) 33 | 34 | # assert content 35 | page_content = includer_schema.replace( 36 | '{filepath}', 37 | included_file.as_posix(), 38 | ) 39 | includer_file.write_text(page_content) 40 | 41 | expected_result = expected_result.replace( 42 | '{filepath}', 43 | included_file.as_posix(), 44 | ) 45 | 46 | assert ( 47 | on_page_markdown( 48 | page_content, 49 | page(includer_file), 50 | tmp_path, 51 | plugin, 52 | ) 53 | == expected_result 54 | ) 55 | 56 | assert len(caplog.records) == 0 57 | 58 | 59 | @pytest.mark.parametrize( 60 | TESTS_ARGUMENTS, 61 | ( 62 | # opening_tag and closing_tag 63 | pytest.param( 64 | '# Header\n\n{! include "{filepath}" !}\n', 65 | 'This must be included.', 66 | '# Header\n\nThis must be included.\n', 67 | {'opening_tag': '{!', 'closing_tag': '!}'}, 68 | id='custom-tag {! ... !}', 69 | ), 70 | pytest.param( 71 | '# Header\n\n{* include "{filepath}" *}\n', 72 | 'This must be included.', 73 | '# Header\n\nThis must be included.\n', 74 | {'opening_tag': '{*', 'closing_tag': '*}'}, 75 | id='custom-tag {* ... *}', 76 | ), 77 | pytest.param( 78 | '# Header\n\n#INC[ include "{filepath}" ]\n', 79 | 'This must be included.', 80 | '# Header\n\nThis must be included.\n', 81 | {'opening_tag': '#INC[', 'closing_tag': ']'}, 82 | id='custom-tag #INC[ ...]', 83 | ), 84 | pytest.param( 85 | '# Header\n\n.^$*+-?{}[]\\|():<>=!/#%,; include "{filepath}" }\n', 86 | 'This must be included.', 87 | '# Header\n\nThis must be included.\n', 88 | {'opening_tag': '.^$*+-?{}[]\\|():<>=!/#%,;', 'closing_tag': '}'}, 89 | id='custom-tag-all-escaped-char', 90 | ), 91 | 92 | # preserve_includer_indent 93 | pytest.param( 94 | ' {% include "{filepath}" %}', 95 | 'foo\nbar\n', 96 | ' foo\n bar\n', 97 | {}, 98 | id='default-preserve_includer_indent', 99 | ), 100 | pytest.param( 101 | ' {% include "{filepath}" %}', 102 | 'foo\nbar\n', 103 | ' foo\nbar\n', 104 | {'preserve_includer_indent': False}, 105 | id='custom-preserve_includer_indent', 106 | ), 107 | 108 | # dedent 109 | pytest.param( 110 | '{% include "{filepath}" %}', 111 | 'foo\n bar\n', 112 | 'foo\n bar\n', 113 | {}, 114 | id='default-dedent', 115 | ), 116 | pytest.param( 117 | '{% include "{filepath}" %}', 118 | ' foo\n bar\n', 119 | 'foo\nbar\n', 120 | {'dedent': True}, 121 | id='custom-dedent', 122 | ), 123 | 124 | # trailing_newlines 125 | pytest.param( 126 | '{% include "{filepath}" %}', 127 | 'foo\n\n\n', 128 | 'foo\n\n\n', 129 | {}, 130 | id='default-trailing_newlines', 131 | ), 132 | pytest.param( 133 | '{% include "{filepath}" %}', 134 | 'foo\n\n\n', 135 | 'foo', 136 | {'trailing_newlines': False}, 137 | id='custom-trailing_newlines', 138 | ), 139 | 140 | # comments 141 | pytest.param( 142 | '{% include-markdown "{filepath}" %}', 143 | 'foo\n', 144 | 'foo\n', 145 | {'comments': False}, 146 | id='default-comments', 147 | ), 148 | pytest.param( 149 | '{% include-markdown "{filepath}" comments=true %}', 150 | 'foo\n', 151 | '\nfoo\n\n', 152 | {}, 153 | id='custom-comments', 154 | ), 155 | 156 | # directives 157 | pytest.param( 158 | '{% foo "{filepath}" %}bar\n', 159 | 'baz\n', 160 | 'baz\nbar\n', 161 | {'comments': False, 'directives': {'include-markdown': 'foo'}}, 162 | id='custom-include-markdown-directive', 163 | ), 164 | pytest.param( 165 | '{% my-include "{filepath}" %}bar\n', 166 | 'baz\n', 167 | 'baz\nbar\n', 168 | {'comments': False, 'directives': {'include': 'my-include'}}, 169 | id='custom-include-directive', 170 | ), 171 | pytest.param( 172 | '{% foo "{filepath}" %}bar\n{% include-markdown "{filepath}" %}', 173 | 'baz\n', 174 | '{% foo "{filepath}" %}bar\nbaz\n', 175 | {'comments': False, 'directives': {'non-existent': 'foo'}}, 176 | id='default-include-markdown-directive', 177 | ), 178 | pytest.param( 179 | '{% foo "{filepath}" %}bar\n{% include "{filepath}" %}', 180 | 'baz\n', 181 | '{% foo "{filepath}" %}bar\nbaz\n', 182 | {'comments': False, 'directives': {'non-existent': 'foo'}}, 183 | id='default-include-directive', 184 | ), 185 | ), 186 | indirect=['plugin'], 187 | ) 188 | def test_config_options( 189 | includer_schema, 190 | content_to_include, 191 | expected_result, 192 | plugin, 193 | page, 194 | caplog, 195 | tmp_path, 196 | ): 197 | return _run_test( 198 | includer_schema, 199 | content_to_include, 200 | expected_result, 201 | plugin, 202 | page, 203 | caplog, 204 | tmp_path, 205 | ) 206 | 207 | 208 | @unix_only 209 | @pytest.mark.parametrize( 210 | TESTS_ARGUMENTS, 211 | ( 212 | # encoding 213 | pytest.param( 214 | '# Header\n\n{% include "{filepath}" %}', 215 | 'bóg wąż wąską dróżką', 216 | '# Header\n\nbóg wąż wąską dróżką', 217 | {}, 218 | id='default-encoding', 219 | ), 220 | pytest.param( 221 | '# Header\n\n{% include "{filepath}" %}', 222 | 'bóg wąż wąską dróżką', 223 | '# Header\n\nbĂłg wÄ…ĹĽ wÄ…skÄ… dróżkÄ…', 224 | {'encoding': 'cp1250'}, 225 | id='custom-encoding', 226 | ), 227 | ), 228 | indirect=['plugin'], 229 | ) 230 | def test_config_encoding_option( 231 | includer_schema, 232 | content_to_include, 233 | expected_result, 234 | plugin, 235 | page, 236 | caplog, 237 | tmp_path, 238 | ): 239 | return _run_test( 240 | includer_schema, 241 | content_to_include, 242 | expected_result, 243 | plugin, 244 | page, 245 | caplog, 246 | tmp_path, 247 | ) 248 | -------------------------------------------------------------------------------- /tests/test_unit/test_encoding.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from mkdocs_include_markdown_plugin.event import on_page_markdown 4 | from testing_helpers import parametrize_directives, unix_only 5 | 6 | 7 | @parametrize_directives 8 | def test_encoding(directive, page, tmp_path, plugin): 9 | page_to_include_file = tmp_path / 'included.md' 10 | page_to_include_file.write_text('''Á 11 | 12 | Content to include 13 | 14 | É 15 | ''') 16 | 17 | with pytest.raises(UnicodeDecodeError): 18 | on_page_markdown( 19 | f'''{{% 20 | {directive} "{page_to_include_file}" 21 | start='' 22 | end="" 23 | encoding="ascii" 24 | %}}''', 25 | page(tmp_path / 'includer.md'), 26 | tmp_path, 27 | plugin, 28 | ) 29 | 30 | 31 | @unix_only 32 | @parametrize_directives 33 | def test_default_encoding(directive, page, tmp_path, plugin): 34 | page_to_include_file = tmp_path / 'included.md' 35 | page_to_include_file.write_text('''Á 36 | 37 | Content to include 38 | 39 | É 40 | ''') 41 | 42 | result = on_page_markdown( 43 | f'''{{% 44 | {directive} "{page_to_include_file}" 45 | start="" 46 | end="" 47 | %}}''', 48 | page(tmp_path / 'includer.md'), 49 | tmp_path, 50 | plugin, 51 | ) 52 | assert result == '\nContent to include\n' 53 | 54 | 55 | @unix_only 56 | @parametrize_directives 57 | def test_explicit_default_encoding(directive, page, tmp_path, plugin): 58 | page_to_include_file = tmp_path / 'included.md' 59 | page_to_include_file.write_text('''Á 60 | 61 | Content to include 62 | 63 | É 64 | ''') 65 | 66 | result = on_page_markdown( 67 | f'''{{% 68 | {directive} "{page_to_include_file}" 69 | start='' 70 | end="" 71 | encoding="utf-8" 72 | %}}''', 73 | page(tmp_path / 'includer.md'), 74 | tmp_path, 75 | plugin, 76 | ) 77 | assert result == '\nContent to include\n' 78 | -------------------------------------------------------------------------------- /tests/test_unit/test_exclude.py: -------------------------------------------------------------------------------- 1 | """Tests for mkdocs-include-markdown-plugin `exclude` setting.""" 2 | 3 | import functools 4 | import os 5 | import re 6 | 7 | import pytest 8 | from mkdocs.exceptions import PluginError 9 | 10 | from mkdocs_include_markdown_plugin.event import on_page_markdown 11 | from testing_helpers import parametrize_directives, unix_only 12 | 13 | 14 | @unix_only 15 | @parametrize_directives 16 | @pytest.mark.parametrize( 17 | ('filenames', 'exclude', 'exclude_prefix', 'expected_result'), 18 | ( 19 | pytest.param( 20 | ('foo', 'bar', 'baz'), 21 | f'content{os.sep}foo*', 22 | True, 23 | 'bar\nbaz\n', 24 | id='ignore-by-glob', 25 | ), 26 | pytest.param( 27 | ('foo', 'bar', 'baz'), 28 | f'content{os.sep}ba*', 29 | True, 30 | 'foo\n', 31 | id='ignore-multiple-by-glob', 32 | ), 33 | pytest.param( 34 | ('foo', 'bar', 'baz'), 35 | '', 36 | True, 37 | 'bar\nbaz\nfoo\n', 38 | id='not-ignore', 39 | ), 40 | pytest.param( 41 | ('foo', 'bar', 'baz'), 42 | '*', 43 | True, 44 | None, 45 | id='ignore-all', 46 | ), 47 | pytest.param( 48 | ('foo', 'bar', 'baz'), 49 | f'..{os.sep}content{os.sep}*', 50 | False, 51 | None, 52 | id='ignore-all-relative', 53 | ), 54 | pytest.param( 55 | ('foo', 'bar', 'baz'), 56 | f'..{os.sep}content{os.sep}b*', 57 | False, 58 | 'foo\n', 59 | id='ignore-relative', 60 | ), 61 | ), 62 | ) 63 | def test_exclude( 64 | page, 65 | tmp_path, 66 | plugin, 67 | caplog, 68 | directive, 69 | filenames, 70 | exclude, 71 | exclude_prefix, 72 | expected_result, 73 | ): 74 | includer_folder = tmp_path / 'includer' 75 | content_folder = tmp_path / 'content' 76 | includer_folder.mkdir() 77 | content_folder.mkdir() 78 | 79 | includer_file = includer_folder / 'main.txt' 80 | files = { 81 | filename: content_folder / filename for filename in filenames 82 | } 83 | 84 | exclude_prefix = f'{tmp_path}{os.sep}' if exclude_prefix else '' 85 | includer_file_content = f'''{{% 86 | {directive} "{tmp_path}{os.sep}content/*" 87 | exclude='{exclude_prefix}{exclude}' 88 | %}}''' 89 | for basename, file in files.items(): 90 | file.write_text(f'{basename}\n') 91 | 92 | includer_file.write_text(includer_file_content) 93 | 94 | func = functools.partial( 95 | on_page_markdown, 96 | includer_file_content, 97 | page(includer_file), 98 | includer_folder, 99 | plugin, 100 | ) 101 | 102 | if expected_result is None: 103 | with pytest.raises(PluginError) as exc: 104 | func() 105 | assert re.match(r'No files found including ', str(exc.value)) 106 | else: 107 | assert func() == expected_result 108 | assert len(caplog.records) == 0 109 | -------------------------------------------------------------------------------- /tests/test_unit/test_glob_include.py: -------------------------------------------------------------------------------- 1 | """Tests for multiple inclusions across directives.""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from mkdocs_include_markdown_plugin.event import on_page_markdown 8 | from testing_helpers import parametrize_directives, unix_only 9 | 10 | 11 | @unix_only 12 | @parametrize_directives 13 | def test_glob_include_absolute(directive, page, tmp_path, plugin): 14 | includer_file = tmp_path / 'includer.txt' 15 | included_01_file = tmp_path / 'included_01.txt' 16 | included_02_file = tmp_path / 'included_02.txt' 17 | 18 | includer_file_content = f'''foo 19 | 20 | {{% 21 | {directive} "./included*.txt" 22 | %}} 23 | 24 | 25 | {{% 26 | {directive} "{tmp_path}{os.sep}included*.txt" 27 | %}} 28 | ''' 29 | 30 | included_01_content = 'bar' 31 | included_02_content = 'baz' 32 | 33 | includer_file.write_text(includer_file_content) 34 | included_01_file.write_text(included_01_content) 35 | included_02_file.write_text(included_02_content) 36 | 37 | expected_result = '''foo 38 | 39 | barbaz 40 | 41 | 42 | barbaz 43 | ''' 44 | 45 | assert on_page_markdown( 46 | includer_file_content, page(includer_file), tmp_path, plugin, 47 | ) == expected_result 48 | 49 | 50 | @unix_only 51 | @parametrize_directives 52 | def test_glob_include_fallback(directive, page, tmp_path, plugin): 53 | includer_file = tmp_path / 'includer.txt' 54 | includes_dir = tmp_path / 'includes' 55 | includes_dir.mkdir() 56 | included_01_file = includes_dir / 'included_01.txt' 57 | included_02_file = includes_dir / 'included_02.txt' 58 | 59 | includer_file_content = f'''foo 60 | 61 | {{% 62 | {directive} "includes/*.txt" 63 | %}} 64 | ''' 65 | 66 | included_01_content = 'bar' 67 | included_02_content = 'baz' 68 | 69 | includer_file.write_text(includer_file_content) 70 | included_01_file.write_text(included_01_content) 71 | included_02_file.write_text(included_02_content) 72 | 73 | expected_result = '''foo 74 | 75 | barbaz 76 | ''' 77 | 78 | assert on_page_markdown( 79 | includer_file_content, page(includer_file), tmp_path, plugin, 80 | ) == expected_result 81 | 82 | 83 | @parametrize_directives 84 | @pytest.mark.parametrize( 85 | ( 86 | 'includer_content', 87 | 'expected_warnings_schemas', 88 | ), 89 | ( 90 | pytest.param( 91 | '''{% 92 | {directive} "./included*.txt" 93 | start="" 94 | end="" 95 | %} 96 | 97 | {% 98 | {directive} "./included*.txt" 99 | start="" 100 | end="" 101 | %} 102 | ''', 103 | [], 104 | id='start-end', 105 | ), 106 | pytest.param( 107 | '''{% 108 | {directive} "./included*.txt" 109 | end="" 110 | %} 111 | ''', 112 | [], 113 | id='end', 114 | ), 115 | 116 | # both start and end specified but not found in files to include 117 | pytest.param( 118 | '''{% 119 | {directive} "./included*.txt" 120 | start="" 121 | end="" 122 | %} 123 | 124 | {% 125 | {directive} "./included*.txt" 126 | start="" 127 | end="" 128 | %} 129 | ''', 130 | [ 131 | ( 132 | "Delimiter end ''" 133 | " of '{directive}' directive" 134 | ' at {includer_file}:9 not detected in' 135 | ' the files {included_file_01}, {included_file_02}' 136 | ), 137 | ( 138 | "Delimiter end ''" 139 | " of '{directive}' directive" 140 | ' at {includer_file}:3 not detected in' 141 | ' the files {included_file_01}, {included_file_02}' 142 | ), 143 | ( 144 | "Delimiter start ''" 145 | " of '{directive}' directive" 146 | ' at {includer_file}:9 not detected in' 147 | ' the files {included_file_01}, {included_file_02}' 148 | ), 149 | ( 150 | "Delimiter start ''" 151 | " of '{directive}' directive" 152 | ' at {includer_file}:3 not detected in' 153 | ' the files {included_file_01}, {included_file_02}' 154 | ), 155 | ], 156 | id='start-end-not-found', 157 | ), 158 | ), 159 | ) 160 | def test_glob_include( 161 | includer_content, 162 | directive, 163 | expected_warnings_schemas, 164 | page, 165 | plugin, 166 | caplog, 167 | tmp_path, 168 | ): 169 | includer_file = tmp_path / 'includer.txt' 170 | included_01_file = tmp_path / 'included_01.txt' 171 | included_02_file = tmp_path / 'included_02.txt' 172 | 173 | includer_file_content = f'''foo 174 | 175 | {includer_content.replace('{directive}', directive)} 176 | ''' 177 | 178 | included_01_content = '''This 01 must appear only without specifying start. 179 | 180 | bar 181 | 182 | This 01 must appear only without specifying end. 183 | ''' 184 | included_02_content = '''This 02 must appear only without specifying start. 185 | 186 | baz 187 | 188 | This 02 must appear only without specifying end. 189 | ''' 190 | 191 | includer_file.write_text(includer_file_content) 192 | included_01_file.write_text(included_01_content) 193 | included_02_file.write_text(included_02_content) 194 | 195 | on_page_markdown( 196 | includer_file_content, page(includer_file), tmp_path, plugin, 197 | ) 198 | 199 | # assert warnings 200 | expected_warnings_schemas = expected_warnings_schemas or [] 201 | expected_warnings = [ 202 | msg_schema.replace( 203 | '{includer_file}', 204 | str(includer_file.relative_to(tmp_path)), 205 | ).replace( 206 | '{included_file_01}', 207 | str(included_01_file.relative_to(tmp_path)), 208 | ).replace( 209 | '{included_file_02}', 210 | str(included_02_file.relative_to(tmp_path)), 211 | ).replace('{directive}', directive) 212 | for msg_schema in expected_warnings_schemas 213 | ] 214 | 215 | for record in caplog.records: 216 | assert record.msg in expected_warnings 217 | assert len(expected_warnings_schemas) == len(caplog.records) 218 | -------------------------------------------------------------------------------- /tests/test_unit/test_include.py: -------------------------------------------------------------------------------- 1 | """``include`` directive tests.""" 2 | 3 | import pytest 4 | 5 | from mkdocs_include_markdown_plugin.event import on_page_markdown 6 | 7 | 8 | @pytest.mark.parametrize( 9 | ( 10 | 'includer_schema', 11 | 'content_to_include', 12 | 'expected_result', 13 | 'expected_warnings_schemas', 14 | ), 15 | ( 16 | pytest.param( 17 | '# Header\n\n{% include "{filepath}" %}\n', 18 | 'This must be included.', 19 | '# Header\n\nThis must be included.\n', 20 | [], 21 | id='simple-case', 22 | ), 23 | 24 | # Newline at the end of the included content 25 | pytest.param( 26 | '# Header\n\n{% include "{filepath}" %}\n', 27 | 'This must be included.\n', 28 | '# Header\n\nThis must be included.\n\n', 29 | [], 30 | id='newline-at-end-of-included', 31 | ), 32 | 33 | # Start and end options 34 | pytest.param( 35 | '''# Header 36 | 37 | {% 38 | include "{filepath}" 39 | start="start here" 40 | end="end here" 41 | %} 42 | ''', 43 | '''This must be ignored. 44 | start hereThis must be included.end here 45 | This must be ignored also. 46 | ''', 47 | '''# Header 48 | 49 | This must be included. 50 | ''', 51 | [], 52 | id='start/end', 53 | ), 54 | 55 | # Start and end options with escaped special characters 56 | pytest.param( 57 | '''# Header 58 | 59 | {% 60 | include "{filepath}" 61 | start="\\tstart here" 62 | end="\\tend here" 63 | %} 64 | ''', 65 | '''This must be ignored. 66 | \tstart hereThis must be included.\tend here 67 | This must be ignored also. 68 | ''', 69 | '''# Header 70 | 71 | This must be included. 72 | ''', 73 | [], 74 | id='start/end (escaped special characters)', 75 | ), 76 | 77 | # Start and end options with unescaped special characters 78 | pytest.param( 79 | '''# Header 80 | 81 | {% 82 | include "{filepath}" 83 | start="\tstart here" 84 | end="\tend here" 85 | %} 86 | ''', 87 | '''This must be ignored. 88 | \tstart hereThis must be included.\tend here 89 | This must be ignored also. 90 | ''', 91 | '''# Header 92 | 93 | This must be included. 94 | ''', 95 | [], 96 | id='start/end (unescaped special characters)', 97 | ), 98 | 99 | # Multiples start and end matchs 100 | pytest.param( 101 | '''{% 102 | include-markdown "{filepath}" 103 | start="" 104 | end="" 105 | %}''', 106 | '''Some text 107 | 108 | 109 | This should be included. 110 | 111 | 112 | This shouldn't be included. 113 | 114 | 115 | This should be included also. 116 | 117 | 118 | Here some text 119 | that should be ignored. 120 | 121 | 122 | 123 | 124 | Etc 125 | 126 | This should be included even if hasn't defined after end tag. 127 | ''', 128 | ''' 129 | This should be included. 130 | 131 | This should be included also. 132 | 133 | This should be included even if hasn't defined after end tag. 134 | ''', 135 | [], 136 | id='multiple-start-end-matchs', 137 | ), 138 | 139 | # Don't specify end and finds start in included content 140 | pytest.param( 141 | '''Foo 142 | {% 143 | include "{filepath}" 144 | start="" 145 | %}''', 146 | '''Some text 147 | 148 | 149 | More text 150 | ''', 151 | '''Foo 152 | 153 | More text 154 | ''', 155 | [], 156 | id='start=foo-end=None', 157 | ), 158 | 159 | # Don't specify start and finds end in included content 160 | pytest.param( 161 | '''Foo 162 | {% 163 | include "{filepath}" 164 | end="" 165 | %}''', 166 | ''' 167 | Some text 168 | 169 | More text 170 | ''', 171 | '''Foo 172 | 173 | Some text 174 | ''', 175 | [], 176 | id='start=None-end=foo', 177 | ), 178 | 179 | # Don't specify end but not finds start in included content 180 | pytest.param( 181 | '''Foo 182 | 183 | {% 184 | include "{filepath}" 185 | start="" 186 | %}''', 187 | '''Some text 188 | ''', 189 | '''Foo 190 | 191 | ''', 192 | [ 193 | ( 194 | "Delimiter start '' of 'include'" 195 | ' directive at {filepath}:3' 196 | ' not detected in the file {included_file}' 197 | ), 198 | ], 199 | id='start=foo (not found)-end=None', 200 | ), 201 | 202 | # Don't specify start but not finds end in included content 203 | pytest.param( 204 | '''Foo 205 | {% 206 | include "{filepath}" 207 | end="" 208 | %}''', 209 | ''' 210 | Some text 211 | ''', 212 | '''Foo 213 | 214 | Some text 215 | ''', 216 | [ 217 | ( 218 | "Delimiter end '' of 'include'" 219 | ' directive at {filepath}:2' 220 | ' not detected in the file {included_file}' 221 | ), 222 | ], 223 | id='start=None-end=foo (not found)', 224 | ), 225 | 226 | # Preserve included indent 227 | pytest.param( 228 | '''1. Ordered list item 229 | {% 230 | include "{filepath}" 231 | preserve-includer-indent=false 232 | %}''', 233 | '''- Unordered sublist item 234 | - Other unordered sublist item''', 235 | '''1. Ordered list item 236 | - Unordered sublist item 237 | - Other unordered sublist item''', 238 | [], 239 | id='preserve included indent', 240 | ), 241 | 242 | # Preserve includer indent 243 | pytest.param( 244 | '''1. Ordered list item 245 | {% 246 | include "{filepath}" 247 | %}''', 248 | '''- First unordered sublist item 249 | - Second unordered sublist item 250 | - Third unordered sublist item''', 251 | '''1. Ordered list item 252 | - First unordered sublist item 253 | - Second unordered sublist item 254 | - Third unordered sublist item''', 255 | [], 256 | id='preserve includer indent', 257 | ), 258 | 259 | # Custom options ordering 260 | pytest.param( 261 | '''1. Ordered list item 262 | {% 263 | include "{filepath}" 264 | preserve-includer-indent=true 265 | end="" 266 | start="" 267 | %}''', 268 | '''- First unordered sublist item 269 | - Second unordered sublist item 270 | - Third unordered sublist item''', 271 | '''1. Ordered list item 272 | - First unordered sublist item 273 | - Second unordered sublist item''', 274 | [], 275 | id='custom options ordering', 276 | ), 277 | 278 | # Content unindentation 279 | pytest.param( 280 | '''# Header 281 | 282 | {% 283 | include "{filepath}" 284 | dedent=true 285 | %} 286 | ''', 287 | ''' - Foo 288 | - Bar 289 | - Baz''', 290 | '''# Header 291 | 292 | - Foo 293 | - Bar 294 | - Baz 295 | ''', 296 | [], 297 | id='dedent=true', 298 | ), 299 | 300 | # Include from URL 301 | pytest.param( 302 | '''# Header 303 | 304 | {% include "https://raw.githubusercontent.com/mondeja/mkdocs-include-markdown-plugin/master/examples/basic/docs/included.md" %} 305 | ''', # noqa: E501 306 | '(not used)\n', 307 | '''# Header 308 | 309 | Some ignored content. 310 | 311 | <--start--> 312 | 313 | Some included content. 314 | 315 | ''', 316 | [], 317 | id='url', 318 | ), 319 | 320 | # Content unindentation + preserve includer indent 321 | pytest.param( 322 | '''# Header 323 | 324 | {% 325 | include "{filepath}" 326 | dedent=true 327 | preserve-includer-indent=true 328 | %} 329 | ''', 330 | ''' - Foo 331 | - Bar 332 | - Baz''', 333 | '''# Header 334 | 335 | - Foo 336 | - Bar 337 | - Baz 338 | ''', 339 | [], 340 | id='dedent=true,preserve-includer-indent=true', 341 | ), 342 | pytest.param( 343 | 'Foo{% include "{filepath}" trailing-newlines=false %}', 344 | '\n', 345 | 'Foo', 346 | [], 347 | id='rstrip-unix-trailing-newlines', 348 | ), 349 | pytest.param( 350 | 'Foo{% include "{filepath}" trailing-newlines=false %}', 351 | '\r\n\r\n', 352 | 'Foo', 353 | [], 354 | id='rstrip-windows-trailing-newlines', 355 | ), 356 | ), 357 | ) 358 | def test_include( 359 | includer_schema, 360 | content_to_include, 361 | expected_result, 362 | expected_warnings_schemas, 363 | page, 364 | plugin, 365 | caplog, 366 | tmp_path, 367 | ): 368 | included_file = tmp_path / 'included.md' 369 | includer_file = tmp_path / 'includer.md' 370 | 371 | included_file.write_text(content_to_include) 372 | includer_file.write_text( 373 | content_to_include.replace('{filepath}', included_file.as_posix()), 374 | ) 375 | 376 | # assert content 377 | page_content = includer_schema.replace( 378 | '{filepath}', included_file.as_posix(), 379 | ) 380 | includer_file.write_text(page_content) 381 | 382 | assert on_page_markdown( 383 | page_content, page(includer_file), tmp_path, plugin, 384 | ) == expected_result 385 | 386 | # assert warnings 387 | expected_warnings_schemas = expected_warnings_schemas or [] 388 | expected_warnings = [ 389 | msg_schema.replace( 390 | '{filepath}', 391 | str(includer_file.relative_to(tmp_path)), 392 | ).replace( 393 | '{included_file}', 394 | str(included_file.relative_to(tmp_path)), 395 | ) for msg_schema in expected_warnings_schemas 396 | ] 397 | 398 | for record in caplog.records: 399 | assert record.msg in expected_warnings 400 | assert len(expected_warnings_schemas) == len(caplog.records) 401 | -------------------------------------------------------------------------------- /tests/test_unit/test_logging.py: -------------------------------------------------------------------------------- 1 | """Logging tests.""" 2 | 3 | import pytest 4 | 5 | from mkdocs_include_markdown_plugin.event import on_page_markdown 6 | from testing_helpers import parametrize_directives 7 | 8 | 9 | @parametrize_directives 10 | @pytest.mark.parametrize('missing_argument', ('start', 'end')) 11 | def test_start_end_arguments_not_found( 12 | directive, 13 | missing_argument, 14 | page, 15 | tmp_path, 16 | plugin, 17 | caplog, 18 | ): 19 | included_file_name = 'included.md' 20 | includer_file_name = 'includer.md' 21 | included_file = tmp_path / included_file_name 22 | includer_file = tmp_path / includer_file_name 23 | 24 | includer_content = f'''# Heading 25 | 26 | {{% 27 | {directive} "{included_file}" 28 | start="" 29 | end="" 30 | %}} 31 | ''' 32 | if missing_argument == 'end': 33 | included_content = 'Included content' 34 | else: 35 | included_content = 'Included content' 36 | 37 | includer_file.write_text(includer_content) 38 | included_file.write_text(included_content) 39 | 40 | expected_result = '''# Heading 41 | 42 | Included content 43 | ''' 44 | 45 | assert on_page_markdown( 46 | includer_content, page(includer_file), tmp_path, plugin, 47 | ) == expected_result 48 | 49 | assert ( 50 | f"Delimiter {missing_argument} '' of" 51 | f" '{directive}' directive at {includer_file_name}:3" 52 | f' not detected in the file {included_file_name}' 53 | ) in caplog.text 54 | -------------------------------------------------------------------------------- /tests/test_unit/test_nested_includes.py: -------------------------------------------------------------------------------- 1 | """Nested inclusion tests.""" 2 | 3 | import pytest 4 | 5 | from mkdocs_include_markdown_plugin.event import on_page_markdown 6 | 7 | 8 | @pytest.mark.parametrize( 9 | ( 10 | 'first_includer_content', 11 | 'second_includer_content', 12 | 'included_content', 13 | 'expected_result', 14 | 'expected_warnings_schemas', 15 | ), 16 | ( 17 | # Includer -> Markdown -> Markdown 18 | pytest.param( 19 | '''# Header 20 | 21 | {% 22 | include-markdown "{filepath}" 23 | %}''', 24 | '''Some text from second includer. 25 | 26 | {% 27 | include-markdown "{filepath}" 28 | %} 29 | ''', 30 | '''Some test from final included.''', 31 | '''# Header 32 | 33 | Some text from second includer. 34 | 35 | Some test from final included. 36 | ''', 37 | [], 38 | id='includer -> markdown -> markdown', 39 | ), 40 | 41 | # Includer -> Markdown -> file 42 | pytest.param( 43 | '''# Header 44 | 45 | {% 46 | include-markdown "{filepath}" 47 | %}''', 48 | '''Some text from second includer. 49 | 50 | {% 51 | include "{filepath}" 52 | %} 53 | ''', 54 | '''Some test from final included.''', 55 | '''# Header 56 | 57 | Some text from second includer. 58 | 59 | Some test from final included. 60 | ''', 61 | [], 62 | id='includer -> markdown -> file', 63 | ), 64 | 65 | # Includer -> file -> file 66 | pytest.param( 67 | '''# Header 68 | 69 | {% 70 | include "{filepath}" 71 | %}''', 72 | '''Some text from second includer. 73 | 74 | {% 75 | include "{filepath}" 76 | %} 77 | ''', 78 | '''Some test from final included.''', 79 | '''# Header 80 | 81 | Some text from second includer. 82 | 83 | Some test from final included. 84 | ''', 85 | [], 86 | id='includer -> file -> file', 87 | ), 88 | 89 | # Includer -> file -> Markdown 90 | pytest.param( 91 | '''# Header 92 | 93 | {% 94 | include "{filepath}" 95 | %}''', 96 | '''Some text from second includer. 97 | 98 | {% 99 | include-markdown "{filepath}" 100 | start="" 101 | end="" 102 | %} 103 | ''', 104 | '''This must be ignored 105 | Some test from final included. 106 | 107 | This must be ignored also 108 | ''', 109 | '''# Header 110 | 111 | Some text from second includer. 112 | 113 | Some test from final included. 114 | ''', 115 | [], 116 | id='includer -> file -> markdown', 117 | ), 118 | 119 | # cumulative_heading_offset 120 | pytest.param( 121 | '''# Header 122 | 123 | {% 124 | include-markdown "{filepath}" 125 | heading-offset=1 126 | %}''', 127 | '''# Header 2 128 | 129 | {% 130 | include-markdown "{filepath}" 131 | heading-offset=1 132 | %} 133 | ''', 134 | '''# Header 3 135 | ''', 136 | '''# Header 137 | 138 | ## Header 2 139 | 140 | ### Header 3 141 | 142 | ''', 143 | [], 144 | id='cumulative_heading_offset', 145 | ), 146 | 147 | # start and end defined in first inclusion but not found 148 | pytest.param( 149 | '''# Header 150 | 151 | {% 152 | include-markdown '{filepath}' 153 | start="" 154 | end="" 155 | %}''', 156 | '''# Header 2 157 | 158 | ''', 159 | '''# Header 3 160 | ''', 161 | '''# Header 162 | 163 | # Header 2 164 | 165 | ''', 166 | [ 167 | ( 168 | "Delimiter start '' of 'include-markdown'" 169 | ' directive at {first_includer_file}:3 not detected' 170 | ' in the file {second_includer_file}' 171 | ), 172 | ( 173 | "Delimiter end '' of 'include-markdown'" 174 | ' directive at {first_includer_file}:3 not detected' 175 | ' in the file {second_includer_file}' 176 | ), 177 | ], 178 | id='start-end-not-found (first-level)', 179 | ), 180 | # start and end defined in second inclusion but not found 181 | pytest.param( 182 | '''# Header 183 | 184 | {% 185 | include-markdown "{filepath}" 186 | %}''', 187 | '''# Header 2 188 | 189 | {% 190 | include-markdown "{filepath}" 191 | start="" 192 | end="" 193 | %}''', 194 | '''# Header 3 195 | 196 | Included content 197 | ''', 198 | '''# Header 199 | 200 | # Header 2 201 | 202 | # Header 3 203 | 204 | Included content 205 | ''', 206 | [ 207 | ( 208 | "Delimiter start '' of 'include-markdown'" 209 | ' directive at {second_includer_file}:3 not detected' 210 | ' in the file {included_file}' 211 | ), 212 | ( 213 | "Delimiter end '' of 'include-markdown'" 214 | ' directive at {second_includer_file}:3 not detected' 215 | ' in the file {included_file}' 216 | ), 217 | ], 218 | id='start-end-not-found (second-level)', 219 | ), 220 | # recursive inclusion disabled with `include` directive 221 | pytest.param( 222 | '''# Header 223 | 224 | {% 225 | include "{filepath}" 226 | recursive=false 227 | %}''', 228 | '''# Header 2 229 | 230 | {% include "{filepath}" %} 231 | ''', 232 | '''# Header 3 233 | 234 | This content must not be included. 235 | ''', 236 | '''# Header 237 | 238 | # Header 2 239 | 240 | {% include "{filepath}" %} 241 | ''', 242 | [], 243 | id='include-recursive=false', 244 | ), 245 | # recursive inclusion disabled with `include-markdown` directive 246 | pytest.param( 247 | '''# Header 248 | 249 | {% 250 | include-markdown "{filepath}" 251 | recursive=false 252 | %}''', 253 | '''# Header 2 254 | 255 | {% include-markdown "{filepath}" %} 256 | ''', 257 | '''# Header 3 258 | 259 | This content must not be included. 260 | ''', 261 | '''# Header 262 | 263 | # Header 2 264 | 265 | {% include-markdown "{filepath}" %} 266 | ''', 267 | [], 268 | id='include-markdown-recursive=false', 269 | ), 270 | ), 271 | ) 272 | def test_nested_include( 273 | first_includer_content, 274 | second_includer_content, 275 | included_content, 276 | expected_result, 277 | expected_warnings_schemas, 278 | page, 279 | plugin, 280 | caplog, 281 | tmp_path, 282 | ): 283 | first_includer_file = tmp_path / 'first-includer.txt' 284 | second_includer_file = tmp_path / 'second-includer.txt' 285 | included_file = tmp_path / 'included.txt' 286 | 287 | first_includer_content = first_includer_content.replace( 288 | '{filepath}', second_includer_file.as_posix(), 289 | ) 290 | second_includer_content = second_includer_content.replace( 291 | '{filepath}', included_file.as_posix(), 292 | ) 293 | expected_result = expected_result.replace( 294 | '{filepath}', included_file.as_posix(), 295 | ) 296 | 297 | first_includer_file.write_text(first_includer_content) 298 | second_includer_file.write_text(second_includer_content) 299 | included_file.write_text(included_content) 300 | 301 | # assert content 302 | assert on_page_markdown( 303 | first_includer_content, page(first_includer_file), tmp_path, plugin, 304 | ) == expected_result 305 | 306 | # assert warnings 307 | expected_warnings_schemas = expected_warnings_schemas or [] 308 | expected_warnings = [ 309 | msg_schema.replace( 310 | '{first_includer_file}', 311 | str(first_includer_file.relative_to(tmp_path)), 312 | ).replace( 313 | '{second_includer_file}', 314 | str(second_includer_file.relative_to(tmp_path)), 315 | ).replace( 316 | '{included_file}', 317 | str(included_file.relative_to(tmp_path)), 318 | ) for msg_schema in expected_warnings_schemas 319 | ] 320 | 321 | for record in caplog.records: 322 | assert record.msg in expected_warnings 323 | assert len(expected_warnings_schemas) == len(caplog.records) 324 | 325 | 326 | def test_nested_include_relpath(page, tmp_path, plugin): 327 | docs_dir = tmp_path / 'docs' 328 | docs_dir.mkdir() 329 | 330 | first_includer_file = tmp_path / 'first-includer.txt' 331 | second_includer_file = docs_dir / 'second-includer.txt' 332 | included_file = tmp_path / 'included.txt' 333 | 334 | first_includer_content = '''# Header 335 | 336 | {% 337 | include-markdown "./docs/second-includer.txt" 338 | %} 339 | ''' 340 | first_includer_file.write_text(first_includer_content) 341 | 342 | second_includer_content = '''Text from second includer. 343 | 344 | {% 345 | include-markdown "../included.txt" 346 | %} 347 | ''' 348 | second_includer_file.write_text(second_includer_content) 349 | 350 | included_file.write_text('Included content.') 351 | 352 | expected_result = '''# Header 353 | 354 | Text from second includer. 355 | 356 | Included content. 357 | 358 | ''' 359 | 360 | assert on_page_markdown( 361 | first_includer_content, 362 | page(first_includer_file), 363 | docs_dir, 364 | plugin, 365 | ) == expected_result 366 | -------------------------------------------------------------------------------- /tests/test_unit/test_process.py: -------------------------------------------------------------------------------- 1 | """String processing tests.""" 2 | 3 | import pytest 4 | 5 | from mkdocs_include_markdown_plugin.cache import Cache 6 | from mkdocs_include_markdown_plugin.process import ( 7 | increase_headings_offset, 8 | read_url, 9 | rewrite_relative_urls, 10 | ) 11 | 12 | 13 | @pytest.mark.parametrize( 14 | ('markdown', 'source_path', 'destination_path', 'expected_result'), 15 | ( 16 | # Markdown Relative Links 17 | pytest.param( 18 | "Here's a [link](CHANGELOG.md) to the changelog.", 19 | 'README', 20 | 'docs/nav.md', 21 | "Here's a [link](../CHANGELOG.md) to the changelog.", 22 | id='relative-link', 23 | ), 24 | pytest.param( 25 | '''Here's a [link whose text is really long and so is broken across 26 | multiple lines](CHANGELOG.md) to the changelog. 27 | ''', 28 | 'README', 29 | 'docs/nav.md', 30 | '''Here's a [link whose text is really long and so is broken across 31 | multiple lines](../CHANGELOG.md) to the changelog. 32 | ''', 33 | id='multiline-link', 34 | ), 35 | pytest.param( 36 | ''' 37 | Check [this link](foobar.md) for more information 38 | ''', 39 | 'docs/includes/feature_a/index.md', 40 | 'docs/setup.md', 41 | ''' 42 | Check [this link](includes/feature_a/foobar.md) for more information 43 | ''', 44 | id='relative-link-down', 45 | ), 46 | pytest.param( 47 | '''Here's a [link](CHANGELOG.md#v1.2.3) to the changelog. 48 | ''', 49 | 'README', 50 | 'docs/nav.md', 51 | '''Here's a [link](../CHANGELOG.md#v1.2.3) to the changelog. 52 | ''', 53 | id='link-with-hash', 54 | ), 55 | pytest.param( 56 | '''Here's a [link][changelog] to the changelog. 57 | 58 | [changelog]: CHANGELOG.md 59 | ''', 60 | 'README', 61 | 'docs/nav.md', 62 | '''Here's a [link][changelog] to the changelog. 63 | 64 | [changelog]: ../CHANGELOG.md 65 | ''', 66 | id='link-reference', 67 | ), 68 | pytest.param( 69 | "Here's a diagram: ![diagram](assets/diagram.png)", 70 | 'README', 71 | 'docs/home.md', 72 | "Here's a diagram: ![diagram](../assets/diagram.png)", 73 | id='image', 74 | ), 75 | pytest.param( 76 | 'Build status: [![Build Status](badge.png)](build/)', 77 | 'README', 78 | 'docs/home.md', 79 | 'Build status: [![Build Status](../badge.png)](../build/)', 80 | id='image-inside-link', 81 | ), 82 | # HTML Relative Links 83 | pytest.param( 84 | ('Here\'s a diagram: diagram'), 86 | 'README', 87 | 'docs/home.md', 88 | ('Here\'s a diagram: diagram'), 90 | id='html-image', 91 | ), 92 | pytest.param( 93 | ('Here\'s a diagram: '), 95 | 'README', 96 | 'docs/home.md', 97 | ('Here\'s a diagram: '), 99 | id='html-source', 100 | ), 101 | pytest.param( 102 | ('Here\'s a diagram: example'), 104 | 'README', 105 | 'docs/home.md', 106 | ('Here\'s a diagram: example'), 108 | id='html-anchor', 109 | ), 110 | pytest.param( 111 | ("Here's a diagram: diagram"), 113 | 'README', 114 | 'docs/home.md', 115 | ("Here's a diagram: diagram"), 117 | id='html-image-single-quote', 118 | ), 119 | pytest.param( 120 | ("Here's a diagram: example"), 122 | 'README', 123 | 'docs/home.md', 124 | ("Here's a diagram: example"), 126 | id='html-anchor-single-quote', 127 | ), 128 | # HTML Relative Links Adverarial tests: 129 | # (attribute contains >, attribute without value, multiple tag in line) 130 | pytest.param( 131 | ('diagramdiagram'), 134 | 'README', 135 | 'docs/home.md', 136 | ('diagramdiagram'), 139 | id='html-image-adverarial-test', 140 | ), 141 | pytest.param( 142 | ('' 143 | 'foo' 144 | 'bar'), 145 | 'README', 146 | 'docs/home.md', 147 | ('' 148 | 'foo' 149 | 'bar'), 150 | id='html-anchor-adverarial-test', 151 | ), 152 | # HTML Relative Links Adversarial test: img no end slash 153 | pytest.param( 154 | ('Here\'s a diagram: diagram'), 156 | 'README', 157 | 'docs/home.md', 158 | ('Here\'s a diagram: diagram'), 160 | id='html-image-no-end-slash', 161 | ), 162 | # Non-relative links 163 | pytest.param( 164 | "Here's a [link](/CHANGELOG.md) to the changelog.", 165 | 'README', 166 | 'docs/nav.md', 167 | "Here's a [link](/CHANGELOG.md) to the changelog.", 168 | id='absolute-link', 169 | ), 170 | pytest.param( 171 | 'A [link](https://example.com/index.html) to the changelog.', 172 | 'README', 173 | 'docs/nav.md', 174 | 'A [link](https://example.com/index.html) to the changelog.', 175 | id='external-link', 176 | ), 177 | pytest.param( 178 | "Here's a [link](https://example.com) to the changelog.", 179 | 'README', 180 | 'docs/nav.md', 181 | "Here's a [link](https://example.com) to the changelog.", 182 | id='external-top-level-link', 183 | ), 184 | pytest.param( 185 | (''), 187 | 'README', 188 | 'docs/home.md', 189 | (''), 191 | id='html-image-external-link', 192 | ), 193 | pytest.param( 194 | (''), 196 | 'README', 197 | 'docs/home.md', 198 | (''), 200 | id='html-anchor-external-link', 201 | ), 202 | pytest.param( 203 | '', 204 | 'README', 205 | 'docs/home.md', 206 | '', 207 | id='html-anchor-external-top-level-link', 208 | ), 209 | pytest.param( 210 | '''[Homepage](/) [Github](https://github.com/user/repo) 211 | [Privacy policy](/privacy)''', 212 | 'README', 213 | 'docs/nav.md', 214 | '''[Homepage](/) [Github](https://github.com/user/repo) 215 | [Privacy policy](/privacy)''', 216 | id='absolute-urls', 217 | ), 218 | pytest.param( 219 | '[contact us](mailto:hello@example.com)', 220 | 'README', 221 | 'docs/nav.md', 222 | '[contact us](mailto:hello@example.com)', 223 | id='mailto-urls', 224 | ), 225 | pytest.param( 226 | '''Some text before 227 | 228 | ```cpp 229 | // Some code in which rewrites shouldn't be proccessed. 230 | // https://github.com/mondeja/mkdocs-include-markdown-plugin/issues/78 231 | const auto lambda = []() { .... }; 232 | ``` 233 | ''', 234 | 'README', 235 | 'examples/lambda.md', 236 | '''Some text before 237 | 238 | ```cpp 239 | // Some code in which rewrites shouldn't be proccessed. 240 | // https://github.com/mondeja/mkdocs-include-markdown-plugin/issues/78 241 | const auto lambda = []() { .... }; 242 | ``` 243 | ''', 244 | id='cpp-likelink-fenced-codeblock', 245 | ), 246 | pytest.param( 247 | ( 248 | 'Text before\n' 249 | ' \n ' 250 | 'const auto lambda = []() { .... };\n \nText after\n' 251 | ), 252 | 'README', 253 | 'examples/lambda.md', 254 | ( 255 | 'Text before\n' 256 | ' \n ' 257 | 'const auto lambda = []() { .... };\n \nText after\n' 258 | ), 259 | id='cpp-likelink-indented-codeblock', 260 | ), 261 | pytest.param( 262 | ( 263 | 'Text before\r\n' 264 | ' \r\n ' 265 | 'const auto lambda = []() { .... };\r\n \r\nText after\r\n' 266 | ), 267 | 'README', 268 | 'examples/lambda.md', 269 | ( 270 | 'Text before\r\n' 271 | ' \r\n ' 272 | 'const auto lambda = []() { .... };\r\n \r\nText after\r\n' 273 | ), 274 | id='cpp-likelink-indented-codeblock-windows-newlines', 275 | ), 276 | pytest.param( 277 | '''``` 278 | [link](CHANGELOG.md) 279 | ``` 280 | ''', 281 | 'README', 282 | 'docs/nav.md', 283 | '''``` 284 | [link](CHANGELOG.md) 285 | ``` 286 | ''', 287 | id='exclude-fenced-code-blocks', 288 | ), 289 | pytest.param( 290 | '''``` 291 | diagram 292 | example 293 | ``` 294 | ''', 295 | 'README', 296 | 'docs/nav.md', 297 | '''``` 298 | diagram 299 | example 300 | ``` 301 | ''', 302 | id='exclude-fenced-code-blocks-html', 303 | ), 304 | pytest.param( 305 | ( 306 | ' \n' 307 | ' [link](CHANGELOG.md)\n' 308 | ' \n' 309 | ), 310 | 'README', 311 | 'docs/nav.md', 312 | ( 313 | ' \n' 314 | ' [link](CHANGELOG.md)\n' 315 | ' \n' 316 | ), 317 | id='exclude-indented-code-blocks', 318 | ), 319 | pytest.param( 320 | ( 321 | ' \n' 322 | ' [link](CHANGELOG.md)\n' 323 | ), 324 | 'README', 325 | 'docs/nav.md', 326 | # is rewritten because not newline at end of code block 327 | ( 328 | ' \n' 329 | ' [link](../CHANGELOG.md)\n' 330 | ), 331 | id='exclude-indented-code-blocks-eof', 332 | ), 333 | pytest.param( 334 | ( 335 | ' [link](CHANGELOG.md)\n' 336 | ' \n' 337 | ), 338 | 'README', 339 | 'docs/nav.md', 340 | ( 341 | ' [link](../CHANGELOG.md)\n' 342 | ' \n' 343 | ), 344 | # No newline before, is not an indented code block, see: 345 | # https://spec.commonmark.org/0.28/#indented-code-blocks 346 | id='no-exclude-indented-code-blocks-missing-newline-before', 347 | ), 348 | pytest.param( 349 | ( 350 | ' \n' 351 | ' [link](CHANGELOG.md)\n' 352 | 'Foo\n' 353 | ), 354 | 'README', 355 | 'docs/nav.md', 356 | ( 357 | ' \n' 358 | ' [link](../CHANGELOG.md)\n' 359 | 'Foo\n' 360 | ), 361 | # No newline after, is not an indented code block, see: 362 | # https://spec.commonmark.org/0.28/#indented-code-blocks 363 | id='no-exclude-indented-code-blocks-missing-newline-after', 364 | ), 365 | ), 366 | ) 367 | def test_rewrite_relative_urls( 368 | markdown, 369 | source_path, 370 | destination_path, 371 | expected_result, 372 | ): 373 | assert rewrite_relative_urls( 374 | markdown, 375 | source_path, 376 | destination_path, 377 | ) == expected_result 378 | 379 | 380 | @pytest.mark.parametrize( 381 | ('markdown', 'offset', 'expected_result'), 382 | ( 383 | pytest.param( 384 | '''# Foo 385 | 386 | ```python 387 | # this is a comment 388 | hello = "world" 389 | ``` 390 | 391 | # this is an indented 392 | codeblock 393 | 394 | - This list item has a fenced codeblock inside: 395 | 396 | ``` 397 | # fenced codeblock inside list item 398 | ``` 399 | 400 | # Bar 401 | 402 | Some text 403 | 404 | ## Baz 405 | ''', 406 | 2, 407 | '''### Foo 408 | 409 | ```python 410 | # this is a comment 411 | hello = "world" 412 | ``` 413 | 414 | # this is an indented 415 | codeblock 416 | 417 | - This list item has a fenced codeblock inside: 418 | 419 | ``` 420 | # fenced codeblock inside list item 421 | ``` 422 | 423 | ### Bar 424 | 425 | Some text 426 | 427 | #### Baz 428 | ''', 429 | id='```', 430 | ), 431 | pytest.param( 432 | '''# Foo 433 | 434 | ~~~python 435 | # this is a comment 436 | hello = "world" 437 | ~~~ 438 | 439 | # Bar 440 | 441 | Some text 442 | 443 | ## Baz 444 | ''', 445 | 3, 446 | '''#### Foo 447 | 448 | ~~~python 449 | # this is a comment 450 | hello = "world" 451 | ~~~ 452 | 453 | #### Bar 454 | 455 | Some text 456 | 457 | ##### Baz 458 | ''', 459 | id='~~~', 460 | ), 461 | pytest.param( 462 | '''# Foo 463 | 464 | ~~~python 465 | # this is a comment 466 | hello = "world" 467 | ~~~ 468 | 469 | # Bar 470 | 471 | Some text 472 | 473 | ## Baz 474 | 475 | ``` 476 | # another comment 477 | ``` 478 | 479 | # Qux 480 | ''', 481 | 1, 482 | '''## Foo 483 | 484 | ~~~python 485 | # this is a comment 486 | hello = "world" 487 | ~~~ 488 | 489 | ## Bar 490 | 491 | Some text 492 | 493 | ### Baz 494 | 495 | ``` 496 | # another comment 497 | ``` 498 | 499 | ## Qux 500 | ''', 501 | id='```,~~~', 502 | ), 503 | pytest.param( 504 | '''# Foo 505 | 506 | # this is a comment 507 | hello = "world" 508 | 509 | # Bar 510 | 511 | # another comment 512 | 513 | \t# comment in tabbed indented codeblock\r\n 514 | ## Qux 515 | ''', 516 | 1, 517 | '''## Foo 518 | 519 | # this is a comment 520 | hello = "world" 521 | 522 | ## Bar 523 | 524 | # another comment 525 | 526 | \t# comment in tabbed indented codeblock\r\n 527 | ### Qux 528 | ''', 529 | id='indented-codeblocks', 530 | ), 531 | ), 532 | ) 533 | def test_dont_increase_heading_offset_inside_fenced_codeblocks( 534 | markdown, 535 | offset, 536 | expected_result, 537 | ): 538 | assert increase_headings_offset(markdown, offset=offset) == expected_result 539 | 540 | 541 | def test_read_url_cached_content(tmp_path): 542 | url = ( 543 | 'https://raw.githubusercontent.com/mondeja/' 544 | 'mkdocs-include-markdown-plugin/master/README.md' 545 | ) 546 | cache_dir = tmp_path.as_posix() 547 | cached_file_name = Cache.generate_unique_key_from_url(url) 548 | cached_file_path = tmp_path / cached_file_name 549 | if cached_file_path.exists(): 550 | cached_file_path.unlink() 551 | 552 | cache = Cache(cache_dir, 600) 553 | content = read_url(url, cache) 554 | assert cached_file_path.exists() 555 | 556 | cached_content = cached_file_path.read_text( 557 | encoding='utf-8', 558 | ).split('\n', 1)[1] 559 | assert content == cached_content 560 | 561 | assert cache.get_(url) == cached_content 562 | assert cache.get_(url) == read_url(url, cache) 563 | cached_file_path.unlink() 564 | -------------------------------------------------------------------------------- /tests/testing_helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import pytest 5 | 6 | 7 | parametrize_directives = pytest.mark.parametrize( 8 | 'directive', 9 | ('include', 'include-markdown'), 10 | ids=('directive=include', 'directive=include-markdown'), 11 | ) 12 | 13 | unix_only = pytest.mark.skipif( 14 | sys.platform.startswith('win'), 15 | reason='Test only supported on Unix systems', 16 | ) 17 | 18 | rootdir = os.path.join(os.path.dirname(__file__), '..') 19 | --------------------------------------------------------------------------------