├── .github ├── FUNDING.yml └── workflows │ ├── build-deploy.yml │ └── chore-reuse-lint.yml ├── .gitignore ├── .justfile ├── LICENSE ├── LICENSES ├── BSD-3-Clause.txt └── CC0-1.0.txt ├── README.md ├── REUSE.toml ├── mkdocs_obsidian_bridge ├── __init__.py └── plugin.py ├── pyproject.toml └── uv.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022 Serhii “GooRoo” Olendarenko 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | # These are supported funding model platforms 6 | 7 | github: # up to 4 GitHub usernames 8 | - GooRoo 9 | 10 | patreon: # Replace with a single Patreon username 11 | open_collective: # Replace with a single Open Collective username 12 | ko_fi: # Replace with a single Ko-fi username 13 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 14 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 15 | liberapay: # Replace with a single Liberapay username 16 | issuehunt: # Replace with a single IssueHunt username 17 | otechie: # Replace with a single Otechie username 18 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 19 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 20 | -------------------------------------------------------------------------------- /.github/workflows/build-deploy.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2025 Serhii “GooRoo” Olendarenko 2 | # SPDX-FileContributor: Serhii Olendarenko 3 | # 4 | # SPDX-License-Identifier: BSD-3-Clause 5 | 6 | name: 'Build: Deploy to PyPI' 7 | 8 | on: 9 | push: 10 | tags: 11 | - 'v*' 12 | 13 | jobs: 14 | build-deploy: 15 | runs-on: ubuntu-latest 16 | environment: 17 | name: PyPI 18 | url: https://pypi.org/project/mkdocs-obsidian-bridge/ 19 | permissions: 20 | id-token: write 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Install uv 26 | uses: astral-sh/setup-uv@v5 27 | with: 28 | version: "0.5.18" 29 | 30 | - name: Set up Python 31 | run: uv python install 32 | 33 | - name: Build package 34 | run: uv build 35 | 36 | - name: Publish package distributions to PyPI 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | -------------------------------------------------------------------------------- /.github/workflows/chore-reuse-lint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2024 Serhii “GooRoo” Olendarenko 2 | # SPDX-FileContributor: Serhii Olendarenko 3 | # 4 | # SPDX-License-Identifier: CC0-1.0 5 | 6 | name: 'Chore: REUSE Compliance' 7 | 8 | on: [push, pull_request] 9 | 10 | jobs: 11 | lint: 12 | name: reuse lint 13 | runs-on: [ubuntu-latest] 14 | steps: 15 | - name: Checkout the repository 16 | uses: actions/checkout@v4 17 | - name: REUSE Compliance Check 18 | uses: fsfe/reuse-action@v4 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022 Serhii “GooRoo” Olendarenko 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | .DS_Store 6 | *.ipynb 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | pip-wheel-metadata/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # Tools 139 | .vscode 140 | .foam 141 | .obsidian 142 | -------------------------------------------------------------------------------- /.justfile: -------------------------------------------------------------------------------- 1 | build: 2 | uv build 3 | 4 | publish: (build) 5 | uvx twine upload -u $(op read "op://Private/PyPI/username") -p $(op read "op://Private/PyPI/Tokens/Armadillo") dist/* 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Serhii Olendarenko 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /LICENSES/BSD-3-Clause.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Serhii Olendarenko 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # [Obsidian][obsidian] ➡️ [MkDocs][mkdocs] Bridge 8 | 9 | [![Made by Ukrainian](https://img.shields.io/static/v1?label=Made%20by&message=Ukrainian&labelColor=1f5fb2&color=fad247&style=flat-square)](https://savelife.in.ua/en/donate-en/#donate-army-card-once) 10 | [![License](https://img.shields.io/github/license/GooRoo/mkdocs-obsidian-bridge?style=flat-square)](LICENSE) 11 | ![REUSE Compliance](https://img.shields.io/reuse/compliance/github.com%2FGooRoo%2Fmkdocs-obsidian-bridge?style=flat-square) 12 | 13 | An MkDocs plugin that helps exporting your [Obsidian](https://obsidian.md) vault as an MkDocs site. 14 | 15 | ## What it does 16 | 17 | I began writing this plugin to simplify exporting my Obsidian vault to GitHub Pages using MkDocs. The plugin is still in development since there are a lot more features that could possibly be implemented, however, currently it has the following features: 18 | 19 | - Auto-expand incomplete [Markdown links](https://help.obsidian.md/How+to/Format+your+notes#Links) 20 | - Auto-expand incomplete [Obsidian's internal links](https://help.obsidian.md/How+to/Internal+link) 21 | - Detect and mark invalid links (to style them differently) 22 | 23 | ### Auto-expanding incomplete links 24 | 25 | By auto-expanding I mean that you don't need to write a full path to the page you are linking to (exactly like in [Obsidian][obsidian]). Consider the following folder structure: 26 | 27 | ``` 28 | docs/ 29 | ├── 2021/ 30 | │ ├── Books.md 31 | │ └── Games.md 32 | └── 2022/ 33 | └── Sport.md 34 | ``` 35 | 36 | If you are editing `Sport.md`, you could write: 37 | ```md 38 | [Books](../2021/Books.md) 39 | ``` 40 | but with this plugin you can just drop the path: 41 | ```md 42 | [Books](Books.md) 43 | ``` 44 | or even write the [Obsidian][obsidian] way: 45 | ```md 46 | [[Books]] 47 | ``` 48 | 49 | #### Name clashes 50 | 51 | What if you have `Books.md` in both 2021 and 2022? 52 | 53 | ``` 54 | docs/ 55 | ├── 2021/ 56 | │ ├── Books.md 57 | │ └── Games.md 58 | └── 2022/ 59 | ├── Books.md 60 | └── Sport.md 61 | ``` 62 | 63 | By default, the plugin tried to find the shortest relative path (again, like [Obsidian][obsidian]), e.g. 64 | ```md 65 | [[Books]] 66 | ``` 67 | is translated into: 68 | ```md 69 | [Books](./Books.md) 70 | ``` 71 | 72 | But you can also give the resolver _a hint_ by specifying the path **partially:** 73 | ```md 74 | [[2021/Books]] 75 | ``` 76 | or 77 | ```md 78 | [Books](2021/Books.md) 79 | ``` 80 | 81 | Both variants work equivalently. 82 | 83 | ## How to enable 84 | 85 | Install the plugin with: 86 | 87 | ```sh 88 | pip install mkdocs-obsidian-bridge 89 | ``` 90 | 91 | The plugin depends on some features of Python 3.10, so this is the minimum required version. 92 | 93 | Then you can enable it in your `mkdocs.yml` config: 94 | 95 | ```yaml 96 | plugins: 97 | - obsidian-bridge 98 | ``` 99 | 100 | ### Embedding of media files 101 | 102 | If you want to have Obsidian-like embedding of audio and video files or even YouTube videos, enable it in your `mkdocs.yml` like this: 103 | 104 | ```yaml 105 | markdown_extensions: 106 | - obsidian_media_mkdocs 107 | ``` 108 | 109 | More information on this feature can be found here: [**GooRoo/obsidian-media**](https://github.com/GooRoo/obsidian-media). 110 | 111 | ### Using callouts 112 | 113 | Looking for Obsidian-style callouts? Enable them in your `mkdocs.yml` like this: 114 | 115 | ```yaml 116 | markdown_extensions: 117 | - obsidian_callouts 118 | ``` 119 | 120 | More information on this feature can be found here: [**GooRoo/obsidian-callouts**](https://github.com/GooRoo/obsidian-callouts). 121 | 122 | ## Why one more plugin? 123 | 124 | I wouldn't ever write this one if I could achieve what I need with other ones. Maybe, I just couldn't find the solution, but here we are. 125 | 126 |
127 | Comparison to others (possibly, outdated) 128 | 129 | ### Differences to [Autolinks Plugin](https://github.com/zachhannum/mkdocs-autolinks-plugin) 130 | 131 | 1. **Autolinks Plugin** doesn't try to resolve the shortest path out of the list of potential candidates. 132 | 2. It also doesn't support incomplete relative paths. In other words, it works only with file names. 133 | 134 | ### Differences to [Roamlinks Plugin](https://github.com/Jackiexiao/mkdocs-roamlinks-plugin) 135 | 136 | This one, actually, was the reason why I started developing my own plugin in the first place. However, it had the following drawbacks for my use-case: 137 | 138 | 1. As well as **Autolinks Plugin**, the **Roamlinks Plugin** does not try to match the best path if there several of those, does it? 139 | 2. Also, in case it can't resolve the `[[Roam link]]`, it leaves it as a text, while [**Obsidian Bridge**](https://github.com/GooRoo/mkdocs-obsidian-bridge) still transforms it into the Markdown link although invalid one. 140 | 141 | ### Differences to [EZLinks Plugin](https://github.com/orbikm/mkdocs-ezlinks-plugin) 142 | 143 | This one looked like a perfect choice for my needs, however: 144 | 145 | 1. I didn't spent much time playing with it, but **EZLinks Plugin** generated incorrect links for me. Probably because it doesn't resolve any incomplete paths as well as two previous plugins. 146 | 2. At the same time, it **does** convert the `[[internal links]]` into actual links. 147 | 3. It has no ability to distinguish between valid and invalid `[[internal links]]`. Maybe it could be solved by another plugin, but I haven't searched for it. 148 | 149 | ### Differences to [WikiLinks](https://python-markdown.github.io/extensions/wikilinks/) extension for [Python-Markdown](https://github.com/Python-Markdown/markdown/) 150 | 151 | 1. I haven't tried this one, but it looks like **WikiLinks** is unable to automatically resolve paths at all without an additional (and a bit cumbersome) config. 152 | 2. Also, not sure if it supports all the [Obsidian][obsidian]'s features. 153 | 154 |
155 | 156 | --- 157 | 158 | ## Advanced topics 159 | 160 | ### Styling of invalid links 161 | 162 |
163 | See for yourself! 164 | 165 | 166 | The plugin translates [Obsidian][obsidian]-style `[[internal links]]` to markdown `[internal links](internal%20links)` even if the resulting link is invalid. If you want to distinguish such links from the rest, you can assign them a custom CSS style. 167 | 168 | In order to do that, you should add an `invalid_link_attributes` config option to your `mkdocs.yml` **AND** enable the `attr_list` Markdown extension: 169 | 170 | ```yaml 171 | markdown_extensions: 172 | - attr_list 173 | 174 | plugins: 175 | - obsidian-bridge: 176 | invalid_link_attributes: 177 | - '.invalid' 178 | 179 | extra_css: 180 | - stylesheets/extra.css 181 | ``` 182 | 183 | The `.invalid` in this example translates to `class="invalid"` HTML attribute accordingly to the rules of [**Attribute Lists**](https://python-markdown.github.io/extensions/attr_list/) extension. 184 | 185 | After that, you can extend `extra.css` with some style (just don't forget to add `extra_css` property to your `mkdocs.yml` too as above): 186 | 187 | ```css 188 | a.invalid { 189 | color: red; 190 | } 191 | ``` 192 | 193 | Alternatively, if your style is going to be simple, you can just write it in the attribute itself as following: 194 | 195 | ```yaml 196 | markdown_extensions: 197 | - attr_list 198 | 199 | plugins: 200 | - obsidian-bridge: 201 | invalid_link_attributes: 202 | - 'style="color: red"' 203 | ``` 204 |
205 | 206 | --- 207 | 208 | ## What's next 209 | 210 | My current preliminary roadmap is the following: 211 | 212 | - [x] [**Embedding of audio/video**](https://help.obsidian.md/Linking+notes+and+files/Embed+files) 213 | - [x] Obsidian's [**callouts**](https://help.obsidian.md/Editing+and+formatting/Callouts) 214 | - [ ] Support for Obsidian's [**nested tags**](https://help.obsidian.md/Editing+and+formatting/Tags#Nested+tags) 215 | - [ ] Obsidian's [**comments**](https://help.obsidian.md/Editing+and+formatting/Basic+formatting+syntax#Comments) `%% ... %%` ➡️ HTML comments `` 216 | 217 | I give no guarantees about the deadlines or whether I implement anything at all. I do it for myself and currently I do see a need, so probably I'll continue. 218 | 219 | ### Feedback 220 | 221 | I do appreciate any kind of constructive feedback. 222 | 223 | * If you found a bug, please, [report it](https://github.com/GooRoo/mkdocs-obsidian-bridge/issues/new). 224 | * If you want to request a feature, please, [post an idea](https://github.com/GooRoo/mkdocs-obsidian-bridge/discussions/new?category=Ideas). 225 | * In all other cases, don't hesitate to [start a discussion](https://github.com/GooRoo/mkdocs-obsidian-bridge/discussions/new). 226 | 227 | 228 | [mkdocs]: https://www.mkdocs.org 229 | [obsidian]: https://obsidian.md 230 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2024 Serhii “GooRoo” Olendarenko 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | version = 1 5 | 6 | [[annotations]] 7 | path = "*.lock" 8 | SPDX-FileCopyrightText= "© 2024 Serhii “GooRoo” Olendarenko" 9 | SPDX-License-Identifier= "CC0-1.0" 10 | 11 | [[annotations]] 12 | path = ".justfile" 13 | SPDX-FileCopyrightText= "© 2024 Serhii “GooRoo” Olendarenko" 14 | SPDX-License-Identifier= "BSD-3-Clause" 15 | -------------------------------------------------------------------------------- /mkdocs_obsidian_bridge/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022 Serhii “GooRoo” Olendarenko 2 | # 3 | # SPDX-License-Identifier: BSD-3-Clause 4 | -------------------------------------------------------------------------------- /mkdocs_obsidian_bridge/plugin.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022 Serhii “GooRoo” Olendarenko 2 | # 3 | # SPDX-License-Identifier: BSD-3-Clause 4 | 5 | import logging 6 | import os 7 | import re 8 | import urllib.parse 9 | from collections import defaultdict 10 | from functools import partial 11 | from pathlib import Path 12 | 13 | import mkdocs.utils 14 | from markdown.extensions.toc import slugify_unicode as md_slugify 15 | from mkdocs.config import base, config_options as co 16 | from mkdocs.config.defaults import MkDocsConfig 17 | from mkdocs.plugins import BasePlugin 18 | from mkdocs.structure.files import Files as MkDocsFiles 19 | 20 | logger = logging.getLogger(f'mkdocs.plugins.{__name__}') 21 | 22 | FilenameToPaths = defaultdict[str, list[Path]] 23 | 24 | 25 | class NoCandidatesError(Exception): 26 | pass 27 | 28 | 29 | class ObsidianBridgeConfig(base.Config): 30 | invalid_link_attributes = co.ListOfItems(co.Type(str), default=[]) 31 | 32 | 33 | class ObsidianBridgePlugin(BasePlugin[ObsidianBridgeConfig]): 34 | ''' 35 | Plugin to make obsidian or incomplete markdown links work. 36 | ''' 37 | 38 | # from https://help.obsidian.md/Files+and+folders/Accepted+file+formats: 39 | OBSIDIAN_FORMATS = [ 40 | 'md', 41 | 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 42 | 'mp3', 'webm', 'wav', 'm4a', 'ogg', '3gp', 'flac', 43 | 'mp4', 'webm', 'ogv', 'mov', 'mkv', 44 | 'pdf' 45 | ] 46 | 47 | def __init__(self): 48 | self.file_map: FilenameToPaths | None = None 49 | self.attr_list: str | None = None 50 | 51 | def on_config(self, config: MkDocsConfig) -> MkDocsConfig: 52 | # mkdocs defaults 53 | toc = { 54 | 'slugify': md_slugify, 55 | 'separator': '-' 56 | } 57 | 58 | # update from the config if changed by a user 59 | toc |= config.mdx_configs.get('toc', dict()) 60 | 61 | self.toc_slugify = partial(toc['slugify'], 62 | separator=toc['separator'] 63 | ) 64 | 65 | if len(self.config.invalid_link_attributes) > 0: 66 | if 'attr_list' in config.markdown_extensions: 67 | self.attr_list = ' '.join(self.config.invalid_link_attributes) 68 | else: 69 | logger.warning( 70 | '''[ObsidianBridgePlugin] The 'invalid_link_attributes' from the 'mkdocs.yml' will ''' 71 | '''be ignored. You need to also enable the 'attr_list' Markdown extension.''' 72 | ) 73 | self.attr_list = None 74 | 75 | return config 76 | 77 | def on_files(self, files: MkDocsFiles, *, config: MkDocsConfig) -> MkDocsFiles: 78 | '''Initialize the filename lookup dict if it hasn't already been initialized''' 79 | if self.file_map is None: 80 | self.file_map = self.build_file_map(files) 81 | return files 82 | 83 | def on_page_markdown(self, markdown: str, page, config: MkDocsConfig, files: MkDocsFiles, **kwargs) -> str: 84 | # Getting the root location of markdown source files 85 | self.docs_dir = Path(config.docs_dir) 86 | 87 | # Getting the page path that we are linking from 88 | page_path = Path(page.file.abs_src_path) 89 | 90 | # Look for matches and replace 91 | markdown = self.process_markdown_links(page_path, markdown) 92 | markdown = self.process_obsidian_links(page_path, markdown) 93 | 94 | return markdown 95 | 96 | def slugify(self, text: str | None) -> str: 97 | assert self.toc_slugify is not None 98 | 99 | if text is None or text == '': 100 | return '' 101 | else: 102 | return f'#{self.toc_slugify(text)}' 103 | 104 | def with_attrs(self, link: str, *, when=True) -> str: 105 | if self.attr_list is not None and when is True: 106 | return f'{link}{{: {self.attr_list}}}' 107 | else: 108 | return link 109 | 110 | def build_file_map(self, files: MkDocsFiles) -> FilenameToPaths: 111 | result = defaultdict(list) 112 | for file in files: 113 | filepath = Path(file.abs_src_path) 114 | filename = filepath.name 115 | result[filename].append(Path(file.abs_src_path)) 116 | return result 117 | 118 | def best_path(self, page_dir: Path, candidates: list[Path]) -> Path: 119 | '''Return the shortest path from the list of path candidates relatively to the page_dir.''' 120 | assert page_dir.is_absolute() 121 | assert all(c.is_absolute() for c in candidates) 122 | 123 | match len(candidates): 124 | case 1: 125 | return Path(os.path.relpath(candidates[0], page_dir)) 126 | 127 | case n if n > 1: 128 | # transform all candidates to paths relative to the page directory 129 | rel_paths = [Path(os.path.relpath(c, page_dir)) for c in candidates] 130 | # choose the first shortest relative path 131 | return min(rel_paths, key=lambda p: len(p.parts)) 132 | 133 | case _: 134 | raise NoCandidatesError() 135 | 136 | def find_best_path(self, link_filepath: Path, page_path: Path) -> Path | None: 137 | def match_link_filepath(p: Path) -> bool: 138 | return p.as_posix().endswith(link_filepath.as_posix()) 139 | 140 | assert page_path.is_absolute() 141 | assert self.file_map is not None 142 | 143 | # Check if the filename exists in the filename to abs path lookup defaultdict 144 | if link_filepath.name not in self.file_map: 145 | # An if-statement is necessary because self.filename_to_abs_path is a 146 | # defaultdict, so the more pythonic try: except: wouldn't work. 147 | logger.warning( 148 | '''[ObsidianBridgePlugin] Unable to find %s in directory %s''', 149 | link_filepath, 150 | self.docs_dir, 151 | ) 152 | return 153 | 154 | page_dir = page_path.parent 155 | # filter the list of all candidates for the filename, so that only those with a match of the filepath 156 | # from the link are left 157 | path_candidates = [p for p in self.file_map[link_filepath.name] if match_link_filepath(p)] 158 | try: 159 | return self.best_path(page_dir, path_candidates) 160 | except NoCandidatesError: 161 | logger.error('''[ObsidianBridgePlugin] No candidates for filepath '%s' were given.''', link_filepath) 162 | return 163 | 164 | def process_markdown_links(self, page_path: Path, markdown: str) -> str: 165 | ''' 166 | Find Markdown links to relative paths and transform them so that paths become valid 167 | (if possible to find a matching sub-path) 168 | ''' 169 | 170 | assert page_path.is_absolute() 171 | 172 | # For Regex, match groups are: 173 | # 0: Whole markdown link e.g. [Alt-text](url#head "title") 174 | # label: Alt text 175 | # link: Full URL e.g. url + hash anchor + title 176 | # filepath: Filename e.g. filename.md 177 | # fragment: hash anchor e.g. #my-sub-heading-link 178 | # title: (image) title in quotation marks 179 | MARKDOWN_LINK = ( 180 | # part in brackets: 181 | r'(?:\!\[\]|\[(?P