diff --git a/.github/find_unused_image.py b/.github/find_unused_image.py new file mode 100644 index 0000000..e6cff43 --- /dev/null +++ b/.github/find_unused_image.py @@ -0,0 +1,65 @@ +from pathlib import Path +import os +import argparse +from typing import Optional +import yaml + + +def find_unused_media(img_path: Optional[Path] = None, dry_run: bool = False): + # load mkdocs.yml + with open("mkdocs.yml", "r", encoding="utf-8") as f: + # remove all !! pairs from the yaml file + data = f.read() + data = data.replace("!!", "") + config = yaml.safe_load(data) + + docs_dir = Path.cwd() / Path(config.get("docs_dir", "docs")) + assets_dir = Path(docs_dir, config["extra"]["attachments"]) + print(f"Looking for unused images in {assets_dir}...") + if img_path: + assets_dir = img_path + + images = [ + file + for file in assets_dir.rglob("*") + if file.is_file() and file.suffix in [".png", ".jpg", ".jpeg", ".gif", ".svg"] + ] + md_files = [file for file in docs_dir.rglob("*.md") if file.is_file()] + + # Search for images in markdown files + + used_images = [] + + for md_file in md_files: + for image in images: + with open(md_file, "r", encoding="utf-8") as f: + if image.name in f.read(): + used_images.append(image) + + # compare the two lists + unused_images = [image for image in images if image not in used_images] + + # delete unused images + + if unused_images: + print(f"Found {len(unused_images)} unused images in {assets_dir}. Deleting...") + for image in unused_images: + if not dry_run: + print(image) + os.remove(image) + else: + print(f"Would delete {image}") + else: + print(f"Found no unused images in {assets_dir}.") + + +if __name__ == "__main__": + # use argparse to get the path to the assets folder + parser = argparse.ArgumentParser() + parser.add_argument("--path", type=str, help="Path to the assets folder") + parser.add_argument( + "--dry-run", action="store_true", help="Do not delete unused images" + ) + args = parser.parse_args() + path = Path(args.path) if args.path else None + find_unused_media(img_path=path, dry_run=args.dry_run) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..be24c91 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,55 @@ +name: Publish + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'README.md' + - 'overrides/**' + - 'docs/**' + - 'mkdocs.yml' + - 'uv.lock' + repository_dispatch: + types: [build] + +permissions: + contents: write + pages: write + id-token: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + fetch-depth: 0 + submodules: 'recursive' + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + - name: "Submodule fetching" + continue-on-error: true + run: | + git submodule update --init --recursive --checkout -f --remote -- "docs" + git config --global user.name "GitHub Action" + git config --global user.email "noreply@github.com" + git commit -am "chore (update): fetch submodule" + git push + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: "pip" + - name: Install dependencies + run: uv sync + - name: Build + run: | + uv run mkdocs gh-deploy --force + diff --git a/.github/workflows/find_unused_images.yml b/.github/workflows/find_unused_images.yml new file mode 100644 index 0000000..e8cfaf4 --- /dev/null +++ b/.github/workflows/find_unused_images.yml @@ -0,0 +1,51 @@ +name: Repository maintenance +on: + workflow_call: + inputs: + CLEAN: + description: "Clean unused images" + required: false + default: true + type: boolean + DRY_RUN: + description: "Dry run" + required: false + default: false + type: boolean + BRANCH: + type: string + description: 'Branch to push changes to' + required: false + default: 'main' + secrets: + GH_PAT: + required: true + author_email: + required: false + description: "The author email" + author_name: + required: false + description: "The author name" +jobs: + unused_images: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.13 + cache: pipenv + - name: Clean + if: inputs.CLEAN + run: | + echo "Cleaning" + pipenv install pyyaml + pipenv run python .github/find_unused_image.py + - name: Commit + if: inputs.CLEAN && inputs.DRY_RUN == false + uses: actions-js/push@master + with: + github_token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN}} + author_email: ${{ secrets.AUTHOR_EMAIL || 'github-actions[bot]@users.noreply.github.com' }} + author_name: ${{ secrets.AUTHOR_NAME || 'github-actions[bot]' }} + branch: ${{ inputs.BRANCH }} diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml new file mode 100644 index 0000000..3ff6b34 --- /dev/null +++ b/.github/workflows/generate.yml @@ -0,0 +1,134 @@ +name: Generate website + +on: + workflow_dispatch: + inputs: + site_name: + type: string + required: true + description: "The name of the site" + site_url: + type: string + required: true + description: "The url of the site" + site_description: + type: string + required: true + description: "The description of the site" + site_author: + type: string + required: true + description: "The author of the site" + language: + type: string + required: true + description: "The language of the site" + auto_h1: + type: boolean + required: false + description: "Automatically add h1 to pages" + default: false + comments: + type: boolean + required: false + description: "Enable comments" + default: false + auto_merge: + description: "Automatically merge the pull request" + required: false + default: false + type: boolean + +permissions: + contents: write + pull-requests: write + +jobs: + run: + runs-on: ubuntu-latest + steps: + - name: Print github.event.inputs + run: | + echo "📫 Commands arguments" >> $GITHUB_STEP_SUMMARY + echo "- site_name: ${{ github.event.inputs.site_name }}" >> $GITHUB_STEP_SUMMARY + echo "- site_url: ${{ github.event.inputs.site_url }}" >> $GITHUB_STEP_SUMMARY + echo "- site_description: ${{ github.event.inputs.site_description }}" >> $GITHUB_STEP_SUMMARY + echo "- site_author: ${{ github.event.inputs.site_author }}" >> $GITHUB_STEP_SUMMARY + echo "- language: ${{ github.event.inputs.language }}" >> $GITHUB_STEP_SUMMARY + echo "- auto_h1: ${{ github.event.inputs.auto_h1 }}" >> $GITHUB_STEP_SUMMARY + echo "- comments: ${{ github.event.inputs.comments }}" >> $GITHUB_STEP_SUMMARY + echo "- template_type: ${{ github.event.inputs.template_type }}" >> $GITHUB_STEP_SUMMARY + echo "ACT: ${{ env.ACT }}" + - name: checkout repo + uses: actions/checkout@v4 + - name: Install python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies + run: | + pip install pydantic requests + - name: Format arguments + id: args + run: + | + auto_h1='' + comments='' + if [ "${{ github.event.inputs.auto_h1 }}" = "true" ]; then + auto_h1='--auto-h1' + fi + if [ "${{ github.event.inputs.comments }}" = "true" ]; then + comments='--comments' + fi + echo "auto_h1=$auto_h1" >> $GITHUB_OUTPUT + echo "comments=$comments" >> $GITHUB_OUTPUT + echo "- auto_h1: $auto_h1" >> $GITHUB_STEP_SUMMARY + echo "- comments: $comments" >> $GITHUB_STEP_SUMMARY + - name: Generate files + run: | + echo "📝 Generate files" + pwd + ls *.py + echo "📝 Generate files" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + python3 generate_template.py "${{github.event.inputs.site_name}}" "${{github.event.inputs.site_url}}" "${{github.event.inputs.site_description}}" "${{github.event.inputs.site_author}}" "${{github.event.inputs.language}}" ${{ steps.args.outputs.auto_h1 }} ${{ steps.args.outputs.comments }} >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + - name: Pull Request + id: cpr + uses: peter-evans/create-pull-request@v5 + with: + commit-message: Generating template + title: "First commit -- Generate template" + body: | + 📫 Commands arguments + - site_name: ${{ github.event.inputs.site_name }} + - site_url: ${{ github.event.inputs.site_url }} + - site_description: ${{ github.event.inputs.site_description }} + - site_author: ${{ github.event.inputs.site_author }} + - language: ${{ github.event.inputs.language }} + - auto_h1: ${{ github.event.inputs.auto_h1 }} + - comments: ${{ github.event.inputs.comments }} + - Generated by [Create Pull Request](https://github.com/peter-evans/create-pull-request) + labels: | + generate + branch: generate + base: "main" + delete-branch: true + token: ${{ secrets.GH_TOKEN }} + - name: AutoMerging + id: automerge + if: ${{ inputs.AUTO_MERGE }} + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + gh pr merge ${{ steps.cpr.outputs.pull-request-number }} --squash + echo "Pull Request merged" + echo "done=true" >> $GITHUB_OUTPUT + - name: Delete update branch if possible + if: ${{ inputs.AUTO_MERGE && steps.automerge.outputs.done == 'true' }} + run: | + # verify if the branch exists + if [[ $(git ls-remote --heads origin generate | wc -l) -eq 1 ]]; then + git push origin --delete generate + fi \ No newline at end of file diff --git a/.github/workflows/index.yml b/.github/workflows/index.yml new file mode 100644 index 0000000..44b6438 --- /dev/null +++ b/.github/workflows/index.yml @@ -0,0 +1,43 @@ +name: Index Creation + +on: + workflow_dispatch: + inputs: + category_name: + type: string + required: true + description: "The new folder name" + path: + type: string + required: false + description: "The path of the new folder. Ex: category/subcategory" + description: + type: string + required: false + description: "The description of the category." + toc_hide: + type: boolean + required: false + description: "Hide the toc in the index file." + nav_hide: + type: boolean + required: false + description: "Hide the navigation menu in the index file." + dry_run: + type: boolean + required: false + description: "Do not create new category index, just log." + +jobs: + run: + uses: Enveloppe/actions/.github/workflows/index.yml@main + with: + category_name: ${{ inputs.category_name}} + path: ${{ inputs.path}} + description: ${{ inputs.description}} + toc_hide: ${{ inputs.toc_hide}} + nav_hide: ${{ inputs.nav_hide}} + dry_run: ${{ inputs.dry_run}} + secrets: + GH_PAT: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/optimize.yml b/.github/workflows/optimize.yml new file mode 100644 index 0000000..65fbda2 --- /dev/null +++ b/.github/workflows/optimize.yml @@ -0,0 +1,23 @@ +name: Optimize images +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + optimize-images: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: optimize + uses: calibreapp/image-actions@main + with: + githubToken: ${{ secrets.GITHUB_TOKEN}} + ignorePaths: "_assets/img/avatar_index.gif,_assets/meta/**" + compressOnly: true + - name: "Commit & push" + uses: actions-js/push@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ github.ref }} + message: "[CI] Optimize images" diff --git a/README.md b/README.md index 2dc1591..8dbe64a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# mkdocs -Mkdocs V2 electric bungalow +# Requirements +- Python 3.12 or higher +- [uv](https://docs.astral.sh/uv/) + diff --git a/docs/_assets/icons/file.svg b/docs/_assets/icons/file.svg new file mode 100644 index 0000000..c0c3274 --- /dev/null +++ b/docs/_assets/icons/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/_assets/icons/folder-open.svg b/docs/_assets/icons/folder-open.svg new file mode 100644 index 0000000..6cb81f2 --- /dev/null +++ b/docs/_assets/icons/folder-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/_assets/icons/index.svg b/docs/_assets/icons/index.svg new file mode 100644 index 0000000..72af73e --- /dev/null +++ b/docs/_assets/icons/index.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/_assets/icons/owl.svg b/docs/_assets/icons/owl.svg new file mode 100644 index 0000000..4067c79 --- /dev/null +++ b/docs/_assets/icons/owl.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/docs/_assets/meta/error_404.svg b/docs/_assets/meta/error_404.svg new file mode 100644 index 0000000..4185a95 --- /dev/null +++ b/docs/_assets/meta/error_404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/_assets/meta/favicons.png b/docs/_assets/meta/favicons.png new file mode 100644 index 0000000..3a49d34 Binary files /dev/null and b/docs/_assets/meta/favicons.png differ diff --git a/docs/_assets/meta/logo.png b/docs/_assets/meta/logo.png new file mode 100644 index 0000000..0769673 Binary files /dev/null and b/docs/_assets/meta/logo.png differ diff --git a/docs/_static/css/admonition.css b/docs/_static/css/admonition.css new file mode 100644 index 0000000..e69de29 diff --git a/docs/_static/css/custom_attributes.css b/docs/_static/css/custom_attributes.css new file mode 100644 index 0000000..e69de29 diff --git a/docs/_static/css/customization.css b/docs/_static/css/customization.css new file mode 100644 index 0000000..e69de29 diff --git a/docs/_static/js/mathjax.js b/docs/_static/js/mathjax.js new file mode 100644 index 0000000..a4cd08c --- /dev/null +++ b/docs/_static/js/mathjax.js @@ -0,0 +1,20 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } +}; + +document$.subscribe(() => { + MathJax.startup.output.clearCache() + MathJax.typesetClear() + MathJax.texReset() + MathJax.typesetPromise() +}) + diff --git a/docs/_static/js/snippets.js b/docs/_static/js/snippets.js new file mode 100644 index 0000000..51e7bfe --- /dev/null +++ b/docs/_static/js/snippets.js @@ -0,0 +1,8 @@ +//apply code font on numbers for wiki table admonition + +const admonition = document.querySelectorAll(".admonition.wiki table td"); +admonition.forEach((item) => { + if (item.textContent.match(/^\d+$/)) { + item.style.fontFamily = "var(--md-code-font)"; + } +}); diff --git a/docs/tags.md b/docs/tags.md new file mode 100644 index 0000000..c4a3cae --- /dev/null +++ b/docs/tags.md @@ -0,0 +1 @@ +[TAGS] \ No newline at end of file diff --git a/generate_template.py b/generate_template.py new file mode 100644 index 0000000..44b9c1e --- /dev/null +++ b/generate_template.py @@ -0,0 +1,84 @@ +import argparse +from pathlib import Path +from string import Template +from typing import Literal + +import requests +from pydantic import BaseModel + + +class TemplateModel(BaseModel): + site_name: str + site_url: str + site_description: str + site_author: str + language: str + auto_h1: bool + comments: bool + generate_graph: bool = False + + +class Environment(BaseModel): + env: str + deploy: str + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Generate a template for Obsidian Publisher" + ) + parser.add_argument("site_name", type=str, help="The name of the site") + parser.add_argument("site_url", type=str, help="The url of the site") + parser.add_argument( + "site_description", type=str, help="The description of the site" + ) + parser.add_argument("site_author", type=str, help="The author of the site") + parser.add_argument("language", type=str, help="The language of the site") + parser.add_argument( + "--auto-h1", action="store_true", help="Automatically add h1 to the title" + ) + parser.add_argument( + "--comments", action="store_true", help="Enable comments on the site" + ) + + args = parser.parse_args() + + template = TemplateModel( + template_type=args.template, + site_name=args.site_name, + site_url=args.site_url, + site_description=args.site_description, + site_author=args.site_author, + language=args.language, + auto_h1=args.auto_h1, + comments=args.comments, + generate_graph=True if args.template == "gh_pages" else False, + ) + # download the files + + mkdocs_yaml = Path("mkdocs.yml") + with mkdocs_yaml.open("r", encoding="UTF-8") as f: + mkdocs = f.read() + mkdocs = Template(mkdocs) + s = mkdocs.substitute( + site_name=template.site_name, + site_url=template.site_url, + site_description=template.site_description, + site_author=template.site_author, + language=template.language, + auto_h1=template.auto_h1, + comments=template.comments, + generate_graph=template.generate_graph, + ) + + + with mkdocs_yaml.open("w", encoding="UTF-8") as f: + f.write(s) + + print("Mkdocs template generated:") + print(s) + print("Done!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..09e613c --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,151 @@ +site_name: $site_name # Change this to your site name +site_description: $site_description +site_url: $site_url # Change this to your site URL +site_author: $site_author + +theme: + name: 'material' + logo: _assets/meta/logo.png + favicon: _assets/meta/favicons.png + custom_dir: overrides + font: + text: Karla + code: Ubuntu Mono + language: $language + palette: + # Light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: teal + accent: light blue + toggle: + icon: material/weather-night + name: Passer au mode sombre + + # Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: blue grey + accent: light blue + toggle: + icon: material/weather-sunny + name: Passer en mode clair + features: + - navigation.indexes + - navigation.top + - navigation.tabs + - navigation.tabs.sticky + - search.suggest + - search.highlight +# Extensions +markdown_extensions: + - footnotes + - nl2br + - attr_list + - md_in_html + - sane_lists + - meta + - smarty + - tables + - mdx_breakless_lists + - def_list + - pymdownx.arithmatex: + generic: true + - pymdownx.details + - pymdownx.magiclink + - pymdownx.critic + - pymdownx.caret + - pymdownx.keys + - pymdownx.mark + - pymdownx.tilde + - pymdownx.highlight: + use_pygments: true + anchor_linenums: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + options: + custom_icons: + - overrides/icons + - admonition + - toc: + permalink: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: dataview + class: dataview + format: !!python/name:pymdownx.superfences.fence_div_format + - name: folderv + class: folderv + format: !!python/name:pymdownx.superfences.fence_div_format + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + preserve_tabs: true +plugins: + - search + - meta-descriptions + - awesome-pages + - blog: + blog_dir: blog + blog_toc: true + post_readtime_words_per_minute: 300 + - git-revision-date-localized: + type: date + fallback_to_build_date: true + locale: fr + custom_format: "%A %d %B %Y" + enable_creation_date: true + - ezlinks: + wikilinks: true + - embed_file: + callouts: true + custom-attributes: '_static/css/custom_attributes.css' + - custom-attributes: + file: '_static/css/custom_attributes.css' + - tags: + tags_file: tags.md + - callouts + - glightbox +hooks: + - overrides/hooks/on_page_markdown.py + - overrides/hooks/on_env.py + - overrides/hooks/on_post_page.py +extra_javascript: + - _static/js/mathjax.js + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js + - _static/js/snippets.js + - https://cdn.jsdelivr.net/gh/Enveloppe/_static@refs/heads/master/dist/index.js + +extra_css: + - https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css + - _static/css/admonition.css + - _static/css/custom_attributes.css + - _static/css/customization.css + - https://cdn.jsdelivr.net/gh/Enveloppe/_static@refs/heads/master/dist/styles.css +extra: + SEO: '_assets/meta/logo.png' + comments: $comments + attachments: '_assets/img/' + no-auto-h1: $auto_h1 + social: + - icon: fontawesome/brands/github + link: https://github.com/Enveloppe/ + blog_list: + pagination: true + pagination_message: true + pagination_translation: 'posts in' + no_page_found: 'No pages found!' + hooks: + strip_comments: true + fix_heading: true + icons: + folder: "icons/" #from root _assets ; see overrides/partials/nav-item.html and overrides/partials/tabs-item.html + default: + file: "file.svg" + folder: "folder-open.svg" + index: "index.svg" diff --git a/overrides/404.html b/overrides/404.html new file mode 100644 index 0000000..8f39318 --- /dev/null +++ b/overrides/404.html @@ -0,0 +1,22 @@ +{% extends "main.html" %} + + +{% block content %} + + +{% endblock %} \ No newline at end of file diff --git a/overrides/article.html b/overrides/article.html new file mode 100644 index 0000000..8eb4b3a --- /dev/null +++ b/overrides/article.html @@ -0,0 +1,7 @@ +{# template #} +{% extends "index.html" %} + +{# content #} +{% block content %} +{% include "partials/post-list.html" %} +{% endblock %} \ No newline at end of file diff --git a/overrides/hooks/category.py b/overrides/hooks/category.py new file mode 100644 index 0000000..0848026 --- /dev/null +++ b/overrides/hooks/category.py @@ -0,0 +1,110 @@ +""" +A simple script that help to create a category in the database. Create the folder and the `index.md` file. +Usage : +usage: category.py [-h] [--parent PARENT] [--description DESCRIPTION] [--toc] [--nav] name +positional arguments: + name Name of the category. + +options: + -h, --help show this help message and exit + --parent PARENT Parent category. + --description DESCRIPTION Description of the category. + --toc hide toc + --nav hide nav +""" + +import argparse +import yaml +from pathlib import Path +import re + + +def create_category(path, index_contents): + path.mkdir(parents=True, exist_ok=True) + index_file = Path(path, "index.md") + with open(index_file, "w", encoding="utf-8") as f: + f.write(index_contents.lstrip()) + + +def index_contents(name, description_yaml, hider, description): + index_contents = f""" + --- + index: true{description_yaml} + hidden: true + category: {name} + template: article.html + comments: false + title: {name}{hider} + --- + {description} + """ + index_contents = re.sub(" ", "", index_contents) + return index_contents + + +def resolving_args(args, docs_dir): + if args.parent: + path = Path(docs_dir, args.parent, args.name) + else: + path = Path(docs_dir, args.name) + if args.description: + description_yaml = "\ndescription: " + args.description + description_contents = args.description + else: + description_yaml = "" + description_contents = "" + hider = [] + if args.toc: + hider.append("toc") + if args.nav: + hider.append("navigation") + hider = "\nhide:\n- " + "\n- ".join(hider) if hider else "" + return path, description_yaml, hider, description_contents + + +def main(): + parser = argparse.ArgumentParser( + description="Create a new category your mkdocs documentations from your docs (or configured directory)." + ) + parser.add_argument("name", help="Name of the category.") + parser.add_argument( + "--parent", help='Parent category, in path. Example : "category/subcategory"' + ) + parser.add_argument("--description", help="Description of the category.") + parser.add_argument("--toc", help="hide toc", action="store_true") + parser.add_argument("--nav", help="hide nav", action="store_true") + parser.add_argument( + "--dry-run", + help="Dry run, do not create the category, just log", + action="store_true", + ) + args = parser.parse_args() + + mkdocs_config = Path("mkdocs.yml").resolve() + with open(mkdocs_config, "r", encoding="utf-8") as f: + config = yaml.load(f, Loader=yaml.BaseLoader) + docs_dir = config.get("docs_dir", "docs") + docs_dir = Path(docs_dir).resolve() + path, description_yaml, hider, description_contents = resolving_args(args, docs_dir) + index = index_contents(args.name, description_yaml, hider, description_contents) + if not args.dry_run: + print("📤 Creating category 📤...") + print( + f"\nArguments used :\n- Name: {args.name}\n- Parents: {args.parent}\n- Description: {args.description}\n- Toc: {args.toc}\n- Nav: {args.nav}\n" + ) + print(f"📌 Creating with path : {path}") + create_category(path, index) + print("Category created ! 🎉") + else: + print("🐍 Dry Run 🐍") + print( + f"\nArguments used :\n- Name: {args.name}\n- Parents: {args.parent}\n- Description: {args.description}\n- Toc: {args.toc}\n- Nav: {args.nav}\n" + ) + print(f"🚃 Path : {path}") + print(f"🗃️ Index : {index}") + + +if __name__ == "__main__": + main() + print("Done.") + exit(0) diff --git a/overrides/hooks/on_env.py b/overrides/hooks/on_env.py new file mode 100644 index 0000000..18c6e24 --- /dev/null +++ b/overrides/hooks/on_env.py @@ -0,0 +1,200 @@ +import os +import re +import urllib.parse +import datetime +from pathlib import Path + +import mkdocs.structure.pages +from babel.dates import format_date +from dateutil import parser + + +def get_last_part_URL(url): + """Get the last part of an URL. + + Args: + url (str): the URL + + Returns: + str: the last part of the URL + """ + if not url.endswith("/"): + url = url + "/" + head, tail = os.path.split(url) + return "/" + tail if tail != "" else "" + + +def regex_replace(s, find, replace): + """A non-optimal implementation of a regex filter""" + return re.sub(find, replace, s) + + +def log(text): + """Prints text to the console, in case you need to debug something. + + Using mainly in the template files. + Parameters: + text (str): The text to print. + Returns: + str: An empty string. + """ + print(text) + return "" + + +def time_time(time): + """Converts a time string to a human-readable format. + + Parameters: + time (any): The time string to convert. + Returns: + str|datetime: The converted time. + """ + time = time.replace("-", "/") + time = parser.parse(time).isoformat() + try: + time = datetime.datetime.fromisoformat(time) + return datetime.datetime.strftime(time, "%d %B %Y") + except AttributeError: + return datetime.datetime.strftime(str(time), "%d %B %Y") + except ValueError: + print("value error!") + return time + + +def to_local_time(time, locale): + """Convert to local time. + + Args: + time (any): the time to convert + locale (any): the locale to use + + Returns: + str: the converted time + """ + if isinstance(time, datetime.time) or isinstance(time, datetime.date): + time = time.isoformat() + date = time.replace("-", "/") + date = parser.parse(date) + return format_date(date, locale=locale) + + +def time_todatetime(time): + """convert time to datetime. + + Args: + time (any): time to convert + + Returns: + datetime: the converted time + """ + return parser.parse(time) + + +def time_to_iso(time): + """Convert time to ISO format. + + Args: + time (any): Time to convert + + Returns: + any|str: convert time or the original time if error + """ + if isinstance(time, datetime.time) or isinstance(time, datetime.date): + time = time.isoformat() + time = time.replace("-", "/") + try: + return parser.parse(time).isoformat() + except AttributeError: + return time + + +def page_exists(page): + """Check if a page exists. + + Args: + page (any): The page to check + + Returns: + bool: true if exists + """ + return Path(page).exists() + + +def url_decode(url): + """decode an url in a template. + + Args: + url (any): THE URL + + Returns: + str : the decoded url + """ + return urllib.parse.unquote(url) + + +def value_in_frontmatter(key, metadata): + """Check if a key exists in a dictionnary. + + Args: + key (any): the key to check + metadata (any): the dictionnary to check + + Returns: + bool: true if exists + """ + if key in metadata: + return metadata[key] + else: + return None + + +def icon_exists(path, config): + path = Path(config["docs_dir"]) / "_assets" / path + return Path(path).exists() + + +def replace_by_name(name: list[str] | str): + if isinstance(name, list): + return " ".join(name) + return name + + +def first(name: list[str] | str): + if isinstance(name, list): + return name[0] + return name + + +def links(name: list[str] | str): + if isinstance(name, list): + return "/".join(name) + return name + + +def is_section(path): + if isinstance(path, mkdocs.structure.pages.Page): + return False + return True + + +def on_env(env, config, files, **kwargs): + static_path = os.path.join(config["docs_dir"], "_assets") + if static_path not in env.loader.searchpath: + env.loader.searchpath.append(static_path) + env.filters["convert_time"] = time_time + env.filters["iso_time"] = time_to_iso + env.filters["time_todatetime"] = time_todatetime + env.filters["page_exists"] = page_exists + env.filters["url_decode"] = url_decode + env.filters["log"] = log + env.filters["to_local_time"] = to_local_time + env.filters["value_in_frontmatter"] = value_in_frontmatter + env.filters["regex_replace"] = regex_replace + env.filters["get_last_part_URL"] = get_last_part_URL + env.filters["icon_exists"] = lambda path: icon_exists(path, config) + env.filters["is_section"] = is_section + env.filters["replace_by_name"] = replace_by_name + env.filters["first"] = first + env.filters["links"] = links + return env diff --git a/overrides/hooks/on_page_markdown.py b/overrides/hooks/on_page_markdown.py new file mode 100644 index 0000000..9d498dc --- /dev/null +++ b/overrides/hooks/on_page_markdown.py @@ -0,0 +1,51 @@ +import re + + +def non_breaking_space(markdown): + return re.sub( + "[\u00a0\u1680\u180e\u2000-\u200b\u202f\u205f\u3000\ufeff]", " ", markdown + ) + + +def update_heading(markdown): + file_content = markdown.split("\n") + markdown = "" + code = False + for line in file_content: + if not code: + if line.startswith("```"): + code = True + elif line.startswith("#") and line.count("#") <= 5: + heading_number = line.count("#") + 1 + line = "#" * heading_number + " " + line.replace("#", "") + elif line.startswith("```") and code: + code = True + markdown += line + "\n" + return markdown + + +def strip_comments(markdown): + return re.sub(r"%%.*?%%", "", markdown, flags=re.DOTALL) + + +def fix_tags(metadata): + tags = metadata.get("tags", None) or metadata.get("tag", None) + if tags and isinstance(tags, str): + tags = tags.split("/") + tags = [tag.strip() for tag in tags] + metadata["tags"] = tags + return metadata + + +def on_page_markdown(markdown, files, page, config, **kwargs): + config_hooks = config.get( + "extra", {"hooks": {"strip_comments": True, "fix_heading": False}} + ).get("hooks", {"strip_comments": True, "fix_heading": False}) + if config_hooks.get("strip_comments", True): + markdown = strip_comments(markdown) + if config_hooks.get("fix_heading", False): + markdown = update_heading(markdown) + metadata = fix_tags(page.meta) + page.meta = metadata + markdown = non_breaking_space(markdown) + return markdown diff --git a/overrides/hooks/on_post_page.py b/overrides/hooks/on_post_page.py new file mode 100644 index 0000000..d2a5ce7 --- /dev/null +++ b/overrides/hooks/on_post_page.py @@ -0,0 +1,15 @@ +from bs4 import BeautifulSoup + + +def on_post_page(output, page, config) -> str: + soup = BeautifulSoup(output, "html.parser") + for a_tag in soup.find_all("a", {"class": "ezlinks_not_found"}): + a_tag["class"] = a_tag.get("class", []) + ["ezlinks_not_found"] + new_tag = soup.new_tag("span") + new_tag.string = a_tag.string or a_tag.get("href", "File not found") + for attr in a_tag.attrs: + if attr != "href": + new_tag[attr] = a_tag[attr] + new_tag["src"] = a_tag["href"] + a_tag.replaceWith(new_tag) + return str(soup) diff --git a/overrides/index.html b/overrides/index.html new file mode 100644 index 0000000..08f9424 --- /dev/null +++ b/overrides/index.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block extrahead %} + + + + + + {% include "partials/meta.html" %} +{% endblock %} +{% block content %} + {{ super() }} +{% endblock %} \ No newline at end of file diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..a39ff1c --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block extrahead %} + + + + + + {% include "partials/meta.html" %} +{% endblock %} +{% block content %} + {{ super() }} + {% include "partials/css_class.html" %} + {% include "partials/comments.html" %} + {% set pg_title = 'title' | value_in_frontmatter(page.meta) %} + {% set pg_title = page.meta.title if pg_title is not none else page.title %} + {% set pg_title = page.parent.title if pg_title == 'Index' else pg_title %} + +{% endblock %} \ No newline at end of file diff --git a/overrides/partials/comments.html b/overrides/partials/comments.html new file mode 100644 index 0000000..9e94ea2 --- /dev/null +++ b/overrides/partials/comments.html @@ -0,0 +1,12 @@ +{% set local_comments = "comments" | value_in_frontmatter(page.meta) %} +{% set config_comments = "comments" | value_in_frontmatter(config.extra) %} +{% set config_comments = False if config_comments is none else config.extra["comments"] %} +{% set comments = config_comments if local_comments is none else page.meta['comments'] %} +{% if comments %} + +
+{% endif %} \ No newline at end of file diff --git a/overrides/partials/content.html b/overrides/partials/content.html new file mode 100644 index 0000000..d65296b --- /dev/null +++ b/overrides/partials/content.html @@ -0,0 +1,54 @@ + + + +{% if "material/tags" in config.plugins %} +{% include "partials/tags.html" %} +{% endif %} + + +{% include "partials/actions.html" %} + + +{% if "\x3ch1" not in page.content and not config.extra['no-auto-h1'] %} +

{{ page.title | d(config.site_name, true)}}

+{% endif %} + + +{{ page.content }} + + +{% if page.meta and ( +page.meta.git_revision_date_localized or +page.meta.revision_date +) %} +{% include "partials/source-file.html" %} +{% endif %} + + +{% include "partials/feedback.html" %} + + +{% include "partials/comments.html" %} \ No newline at end of file diff --git a/overrides/partials/css_class.html b/overrides/partials/css_class.html new file mode 100644 index 0000000..5641520 --- /dev/null +++ b/overrides/partials/css_class.html @@ -0,0 +1,17 @@ +{% if page.meta %} + {% if page.meta["cssclasses"] and page.meta["cssclasses"]|length > 0 %} + {% if page.meta["cssclasses"] is iterable %} + {% for cssclasses in page.meta["cssclasses"] %} + + {% endfor %} + {% else %} + + {% endif %} + {% endif %} +{% endif %} \ No newline at end of file diff --git a/overrides/partials/meta.html b/overrides/partials/meta.html new file mode 100644 index 0000000..c232b34 --- /dev/null +++ b/overrides/partials/meta.html @@ -0,0 +1,62 @@ +{% set image = config.site_url ~ config.extra['SEO'] %} +{% set title = config.site_name %} +{% set description = config.site_description %} +{% if page and page.meta %} + {% if not page.is_homepage %} + {% set title = page.title %} + {% elif page.meta.title %} + {% set title = page.meta.title %} + {% endif %} + {% if title == "Index" %} + {% if page.meta.name %} + {% set title = page.meta.name %} + {% else %} + {% set title = page.parent.title %} + {% endif %} + {% endif %} + {% if page.meta.description %} + {% set description = page.meta.description %} + {% endif %} + {% if page.meta.image %} + {% set attachmentsFolder = 'attachments' | value_in_frontmatter(config.extra) %} + {% set attachments = 'assets/img/' if attachmentsFolder is none else config.extra['attachments'] | regex_replace('/$', '') %} + {% set image = config.site_url ~ attachments ~ "/" ~ page.meta.image %} + {% elif page.meta.banner %} + {% set image = page.meta.banner %} + {% endif %} + +{% endif %} + + + + + + + + + + \ No newline at end of file diff --git a/overrides/partials/nav-item.html b/overrides/partials/nav-item.html new file mode 100644 index 0000000..734e463 --- /dev/null +++ b/overrides/partials/nav-item.html @@ -0,0 +1,162 @@ +{% macro render_status(nav_item, type) %} + {% set class = "md-status md-status--" ~ type %} + {% if config.extra.status and config.extra.status[type] %} + + + {% else %} + + {% endif %} +{% endmacro %} +{% macro render_content(nav_item, ref = nav_item) %} + {% if nav_item.is_page and nav_item.meta.icon %} + {% set icons_path = config.extra.icons.folder ~ nav_item.meta.icon ~ ".svg" %} + {% if not icons_path | icon_exists %} + {% set icons_path = config.extra.icons.folder ~ config.extra.icons.default.file %} + {% endif %} + {% include icons_path %} + {% elif nav_item.is_index %} + {% set icons_path = config.extra.icons.folder ~ config.extra.icons.default.index %} + {% include icons_path %} + {% elif nav_item.is_page %} + {% set icons_path = config.extra.icons.folder ~ config.extra.icons.default.file %} + {% include icons_path %} + {% else %} + {% set icons_path = config.extra.icons.folder ~ config.extra.icons.default.folder %} + {% include icons_path %} + {% endif %} + + {{ ref.title }} + + {% if nav_item.is_page and nav_item.meta.status %} + {{ render_status(nav_item, nav_item.meta.status) }} + {% endif %} +{% endmacro %} +{% macro render_pruned(nav_item, ref = nav_item) %} + {% set first = nav_item.children | first %} + {% if first and first.children %} + {{ render_pruned(first, ref) }} + {% else %} + + {{ render_content(ref) }} + {% if nav_item.children | length > 0 %} + + {% endif %} + + {% endif %} +{% endmacro %} +{% macro render(nav_item, path, level) %} + {% set class = "md-nav__item" %} + {% if nav_item.active %} + {% set class = class ~ " md-nav__item--active" %} + {% endif %} + {% if nav_item.pages %} + {% if page in nav_item.pages %} + {% set nav_item = page %} + {% endif %} + {% endif %} + {% if nav_item.children %} + {% set indexes = [] %} + {% if "navigation.indexes" in features %} + {% for nav_item in nav_item.children %} + {% if nav_item.is_index and not index is defined %} + {% set _ = indexes.append(nav_item) %} + {% endif %} + {% endfor %} + {% endif %} + {% if "navigation.tabs" in features %} + {% if level == 1 and nav_item.active %} + {% set class = class ~ " md-nav__item--section" %} + {% set is_section = true %} + {% endif %} + {% if "navigation.sections" in features %} + {% if level == 2 and nav_item.parent.active %} + {% set class = class ~ " md-nav__item--section" %} + {% set is_section = true %} + {% endif %} + {% endif %} + {% elif "navigation.sections" in features %} + {% if level == 1 %} + {% set class = class ~ " md-nav__item--section" %} + {% set is_section = true %} + {% endif %} + {% endif %} + {% if "navigation.prune" in features %} + {% if not is_section and not nav_item.active %} + {% set class = class ~ " md-nav__item--pruned" %} + {% set is_pruned = true %} + {% endif %} + {% endif %} +
  • + {% if not is_pruned %} + {% set checked = "checked" if nav_item.active %} + {% if "navigation.expand" in features and not checked %} + {% set indeterminate = "md-toggle--indeterminate" %} + {% endif %} + + {% if not indexes %} + {% set tabindex = "0" if not is_section %} + + {% else %} + {% set index = indexes | first %} + {% set class = "md-nav__link--active" if index == page %} + + {% endif %} + + {% else %} + {{ render_pruned(nav_item) }} + {% endif %} +
  • + {% elif nav_item == page %} +
  • + {% set toc = page.toc %} + + {% set first = toc | first %} + {% if first and first.level == 1 %} + {% set toc = first.children %} + {% endif %} + {% if toc %} + + {% endif %} + + {{ render_content(nav_item) }} + + {% if toc %} + {% include "partials/toc.html" %} + {% endif %} +
  • + {% else %} +
  • + + {{ render_content(nav_item) }} + +
  • + {% endif %} +{% endmacro %} diff --git a/overrides/partials/post-list.html b/overrides/partials/post-list.html new file mode 100644 index 0000000..2431258 --- /dev/null +++ b/overrides/partials/post-list.html @@ -0,0 +1,257 @@ +{# title #} +{% if not "\x3ch1" in page.content %} +

    {{ page.title | d(config.site_name, true) }}

    +{% endif %} + +{# description #} +

    + {{ page.content }} +

    + +{% if config.extra['blog_list'] %} + {% set config_pagination = config.extra['blog_list'] %} +{% else %} + {% set config_pagination = {'pagination': True, 'pagination_message': True, 'pagination_translation': 'posts in'} %} +{% endif %} + +{# page collection #} +{% set valid_pages=[] %} +{% for p in pages %} + {% set pg = p.page %} + {% if pg.meta and pg.meta.date %} + {% set date = pg.meta.date | iso_time %} + {% set _ = pg.__setattr__("date", date) %} + {% elif page.meta.git_creation_date_localized_raw_iso_date %} + {% set date = page.meta.git_creation_date_localized_raw_iso_date | iso_time %} + {% set _ = pg.__setattr__('date', page.meta.git_creation_date_localized_raw_iso_date) %} + {% else %} + {% set date = build_date_utc | iso_time %} + {% set _ = pg.__setattr__('date', date) %} + {% endif %} + {% set page_category = pg.url | url_decode | replace('/' ~ pg.title ~ '/', '') | lower %} + {% set main_category = page.meta.category | lower %} + {% if main_category in page_category %} + {{ valid_pages.append(pg) or "" }} + {% endif %} +{% endfor %} + +{% set blog_pages=[] %} +{% for pg in valid_pages | sort(attribute = 'date', reverse=True) %} + {% set hidden = true if (pg.meta and pg.meta.hidden) %} + {% if (not pg.is_homepage) and + (not pg.markdown == '') and + (not hidden) and not (pg.title == 'index') + %} + {% set datetime = pg.date |time_todatetime %} + {% set dateti=datetime.strftime('%d/%m/%Y') %} + {% set _ = pg.__setattr__('new_date', dateti) %} + {{ blog_pages.append(pg) or "" }} + {% endif %} +{% endfor %} + +{% if blog_pages|count > 0 %} + {# count the number of pages #} + {% set page_num = 1 %} + {% if config_pagination['pagination'] %} + {% set page_num = (blog_pages|count / 10)|round(method='ceil')|int %} + {% if config_pagination['pagination_translation'] %} + {% set pagination_translation = config_pagination["pagination_translation"] %} + {% endif %} +
    + +
    + +
    + {% endif %} + {# pagination #} +
    + {% for pg_group in blog_pages|slice(page_num) %} + {% if config_pagination['pagination'] %} +
    + {% endif %} + {% for pg in pg_group %} + {% set pg_image = "" %} + {% set pg_category = "" %} + {% set pg_description = "" %} + {% set category_link = "" %} + {% set pg_title = pg.title %} + {% if pg_title == "Index" %} + {% set pg_title = pg.parent.title %} + {% endif %} + {% set main_category = page.meta.category | first | lower %} + {% if pg.meta %} + {% if pg.meta.title %} + {% set pg_title = pg.meta.title %} + {% endif %} + {% if pg.meta.banner %} + {% set pg_image = pg.meta.banner %} + {% elif pg.meta.image %} + {% set attachmentsFolder = 'attachments' | value_in_frontmatter(config.extra) %} + {% set attachmentsFolder = 'assets/img/' if attachmentsFolder is none else config.extra['attachments'] | regex_replace('/$', '') %} + {% set pg_image = config.site_url ~ attachmentsFolder ~ '/' ~ pg.meta.image %} + {% endif %} + {% if pg.meta.category %} + {% set pg_category = pg.meta.category | replace_by_name | lower | replace(main_category, '') | replace('/', '') | title %} + {% if pg_category | length > 1 %} + {% set category_link = config.docs_dir ~ "/" ~ page.meta.category ~ '/' ~ pg_category ~ '/index.md' %} + {% set category_exists = category_link | page_exists %} + {% if category_exists %} + {% set link = pg.meta.category | links %} + {% set link = config.site_url ~ link ~ '/' %} + {% set pg_category = '' ~ pg_category ~ '' %} + {% endif %} + {% endif %} + {% endif %} + {% if pg.meta.description %} + {% set pg_description = pg.meta.description | truncate(200)%} + {% endif %} + {% endif %} +
    +

    + {{ pg_title }} +

    + +
    + {% endfor %} +
    + {% endfor %} +
    + + {% if config_pagination['pagination'] %} +
    + +
    + {% endif %} + + {% if config_pagination["pagination"] and config_pagination["pagination_message"] %} +

    Total : {{ blog_pages|count }} {{ pagination_translation }} {{ page_num }} pages.

    + {% endif %} + + + {% if config_pagination['pagination'] %} + + {% endif %} +{% else %} + +

    + {% set no_page_found = 'no_page_found' | value_in_frontmatter(config.extra['blog_list']) %} + {% set no_page_found = 'No pages found!' if no_page_found is none else config.extra['blog_list']['no_page_found'] %} + {{ no_page_found }} +

    +{% endif %} \ No newline at end of file diff --git a/overrides/partials/source-file.html b/overrides/partials/source-file.html new file mode 100644 index 0000000..1fe740e --- /dev/null +++ b/overrides/partials/source-file.html @@ -0,0 +1,38 @@ +{% import "partials/language.html" as lang with context %} + + + +
    +
    + + + + {% set locale = page.meta.locale or config.locale or config.theme.language or 'en' %} + {% if page.meta.git_revision_date_localized %} + {{ lang.t("source.file.date.updated") }} : + {{ page.meta.git_revision_date_localized }} + + {% elif page.meta.revision_date %} + {{ lang.t("source.file.date.updated") }} : + {{ page.meta.revision_date }} + {% endif %} + {% if page.meta.date %} +
    + {{ lang.t("source.file.date.created") }} : + {{ page.meta.date | to_local_time(locale) }} + {% elif page.meta.git_creation_date_localized %} +
    + {{ lang.t("source.file.date.created") }} : + {{ page.meta.git_creation_date_localized }} + {% endif %} + {% if page.meta.author %} +
    + Auteur : {{ page.meta.author }} + {% endif %} + {% if page.meta.illustration %} +
    + Illustration : {{ page.meta.illustration }} + {% endif %} +
    +
    \ No newline at end of file diff --git a/overrides/partials/tabs-item.html b/overrides/partials/tabs-item.html new file mode 100644 index 0000000..fb7b703 --- /dev/null +++ b/overrides/partials/tabs-item.html @@ -0,0 +1,43 @@ +{% macro render_content(nav_item, ref = nav_item) %} + {% if nav_item == ref or "navigation.indexes" in features %} + {% if nav_item.is_index and nav_item.meta.icon %} + {% set icons_path = config.extra.icons.folder ~ nav_item.meta.icon ~ ".svg" %} + {% if not icons_path | icon_exists %} + {% if nav_item | is_section %} + {% set icons_path = config.extra.icons.folder ~ config.extra.icons.default.folder %} + {% else %} + {% set icons_path = config.extra.icons.folder ~ config.extra.icons.default.file %} + {% endif %} + {% endif %} + {% include icons_path %} + {% else %} + {% set icons_path = config.extra.icons.folder ~ config.extra.icons.default.folder %} + {% include icons_path %} + {% endif %} + {% endif %} + {{ ref.title }} +{% endmacro %} +{% macro render(nav_item, ref = nav_item) %} + {% set class = "md-tabs__item" %} + {% if ref.active %} + {% set class = class ~ " md-tabs__item--active" %} + {% endif %} + {% if nav_item.children %} + {% set first = nav_item.children | first %} + {% if first.children %} + {{ render(first, ref) }} + {% else %} +
  • + + {{ render_content(first, ref) }} + +
  • + {% endif %} + {% else %} +
  • + + {{ render_content(nav_item) }} + +
  • + {% endif %} +{% endmacro %} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9a512d9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,82 @@ +[project] +name = "mkdocs" +version = "0.0.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "babel", + "beautifulsoup4", + "bracex", + "cairocffi", + "cairosvg", + "certifi", + "cffi", + "charset-normalizer", + "click", + "colorama", + "csscompressor", + "cssselect2", + "defusedxml", + "ghp-import", + "gitdb", + "gitpython", + "htmlmin2", + "idna", + "jinja2", + "jsmin", + "markdown", + "markupsafe", + "mdx-breakless-lists", + "mdx-wikilink-plus", + "mergedeep", + "mkdocs", + "mkdocs-awesome-pages-plugin", + "mkdocs-callouts", + "mkdocs-custom-fences", + "mkdocs-custom-tags-attributes", + "mkdocs-embed-file-plugin", + "mkdocs-encryptcontent-plugin", + "mkdocs-get-deps", + "mkdocs-git-revision-date-localized-plugin", + "mkdocs-glightbox", + "mkdocs-material", + "mkdocs-material-extensions", + "mkdocs-meta-descriptions-plugin", + "mkdocs-minify-plugin", + "mkdocs-obsidian-links", + "natsort", + "packaging", + "paginate", + "pathspec", + "pillow", + "platformdirs", + "pycparser", + "pycryptodome", + "pygments", + "pymdown-extensions", + "python-dateutil", + "python-frontmatter", + "pytz", + "pyyaml", + "pyyaml-env-tag", + "regex", + "requests", + "setuptools", + "six", + "smmap", + "soupsieve", + "tinycss2", + "urllib3", + "watchdog", + "wcmatch", + "webencodings", +] + +[tool.ruff.lint] +select = ["PTH", "ANN", "N", "Q", "PL", "E", "F", "I"] +ignore = ["E501"] +exclude = ["tests", "docs", "build", "dist", "venv", "venv3", "venv3.6", "venv3.7", "venv3.8", "venv3.9", "venv3.10", "__pycache__"] + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "double"