+ {{ pg_title }} +
++ {{ pg_description }} +
++ + {% include ".icons/material/calendar.svg" %} + + + {{ pg.new_date }} + + {% if pg_category | length > 1 %} + + + {% include ".icons/material/folder.svg" %} + + {{ pg_category }} + + {% endif %} +
+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 %}
+
+
+ {{ 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 %} + + ++ {{ pg_description }} +
++ + {% include ".icons/material/calendar.svg" %} + + + {{ pg.new_date }} + + {% if pg_category | length > 1 %} + + + {% include ".icons/material/folder.svg" %} + + {{ pg_category }} + + {% endif %} +
+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 %} + + + +