├── .github ├── FUNDING.yml ├── pull_request_template.md └── workflows │ ├── docker.yml │ ├── main.yml │ └── publish-pypi.yml ├── .gitignore ├── DOCKER.md ├── Dockerfile ├── LICENSE ├── README.md ├── TODO.md ├── docs ├── Customization.md ├── Screenshots.md ├── Using the wiki.md ├── _config.yml ├── connect_git_and_online_repo.md ├── development.md ├── environment variables.md ├── images │ ├── __EMPTY__ │ ├── black.png │ ├── editor.png │ ├── file_upload.png │ ├── github_create_notes.png │ ├── graph.png │ ├── homepage.png │ ├── lightmode.png │ ├── list.png │ └── password_protect.png ├── index.md ├── installation.md ├── installation │ ├── docker.md │ └── regular_install.md ├── knowledge graph.md ├── plugins.md ├── wikmd.png └── yaml-configuration.md ├── pyproject.toml ├── src └── wikmd │ ├── __init__.py │ ├── cache.py │ ├── config.py │ ├── git_manager.py │ ├── image_manager.py │ ├── knowledge_graph.py │ ├── plugins │ ├── __init__.py │ ├── alerts │ │ ├── __init__.py │ │ └── alerts.py │ ├── draw │ │ ├── __init__.py │ │ ├── default_draw │ │ ├── draw.py │ │ └── drawings │ │ │ └── .gitkeep │ ├── embed-pages │ │ ├── __init__.py │ │ └── embed-pages.py │ ├── load_plugins.py │ ├── mermaid │ │ ├── __init__.py │ │ └── mermaid.py │ ├── plantuml │ │ ├── __init__.py │ │ └── plantuml.py │ └── swagger │ │ ├── __init__.py │ │ └── swagger.py │ ├── search.py │ ├── static │ ├── css │ │ ├── codemirror.custom.css │ │ ├── codemirror.custom.dark.css │ │ ├── filepond.dark.css │ │ ├── swagger-ui.css │ │ ├── wiki.colors.css │ │ ├── wiki.css │ │ └── wiki.dark.css │ ├── favicon.ico │ ├── fonts │ │ └── .empty │ ├── images │ │ ├── contrast-2-fill.svg │ │ ├── file-copy-white.svg │ │ ├── file-copy.svg │ │ ├── graph_5.svg │ │ ├── knowledge-graph.png │ │ ├── readme-img.png │ │ ├── wiki.gif │ │ ├── wiki.png │ │ └── wiki2.gif │ └── js │ │ ├── .empty │ │ ├── copypaste.js │ │ ├── drawio.js │ │ └── swagger-ui-bundle.js │ ├── templates │ ├── base.html │ ├── content.html │ ├── index.html │ ├── knowledge-graph.html │ ├── list_files.html │ ├── login.html │ ├── new.html │ └── search.html │ ├── utils.py │ ├── web_dependencies.py │ ├── wiki.py │ ├── wiki_template │ ├── Features.md │ ├── How to use the wiki.md │ ├── Markdown cheatsheet.md │ ├── Using the version control system.md │ └── homepage.md │ └── wikmd-config.yaml └── tests ├── test_basics.py ├── test_plugins.py └── test_search.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [linbreux] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | _Provide a short overview ..._ 3 | 4 | ### Details 5 | _Context to describe changes if necessary_ 6 | 7 | ### Checks 8 | - [ ] In case of new feature, add short overview in ```docs/``` 9 | - [ ] Tested changes 10 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | wikmd_tags: ${{ steps.meta.outputs.tags }} 18 | json: ${{ steps.meta.outputs.json }} 19 | strategy: 20 | matrix: 21 | architecture: 22 | - linux/amd64 23 | # - linux/arm64 24 | steps: 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v2 27 | 28 | - name: Docker meta 29 | id: meta 30 | uses: docker/metadata-action@v4 31 | with: 32 | images: linbreux/wikmd 33 | tags: | 34 | type=ref,event=pr 35 | type=ref,event=tag 36 | type=edge 37 | 38 | - name: Check docker image tag 39 | run: echo "Creating Docker image ${{ steps.meta.outputs.tags }}" 40 | 41 | - name: Build & export 42 | uses: docker/build-push-action@v3 43 | with: 44 | file: Dockerfile 45 | push: false 46 | tags: ${{ steps.meta.outputs.tags }} 47 | outputs: type=docker,dest=/tmp/wikmd.tar 48 | 49 | - name: Upload artifact 50 | uses: actions/upload-artifact@v3 51 | with: 52 | name: wikmd 53 | path: /tmp/wikmd.tar 54 | 55 | test: 56 | runs-on: ubuntu-latest 57 | needs: build 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | tag: ${{ fromJSON(needs.build.outputs.json).tags }} 62 | name: "test-${{ matrix.tag }}" 63 | steps: 64 | - name: Set up Docker Buildx 65 | uses: docker/setup-buildx-action@v2 66 | 67 | - name: Download artifact 68 | uses: actions/download-artifact@v3 69 | with: 70 | name: wikmd 71 | path: /tmp 72 | 73 | - name: Load image 74 | run: | 75 | docker load --input /tmp/wikmd.tar 76 | docker image ls -a 77 | 78 | # Start a default wikmd container 79 | - name: Start wikmd 80 | run: docker run -d --name wikmd -p 5000:5000 ${{ matrix.tag }} 81 | 82 | # Wait for wikmd to be up and running 83 | - name: Sleep 84 | uses: jakejarvis/wait-action@master 85 | with: 86 | time: '20s' 87 | 88 | # Print some debugging 89 | - name: Check running containers 90 | run: docker ps -a 91 | 92 | - name: Check docker logs 93 | run: docker logs wikmd 94 | 95 | # Check that wikmd is up and running 96 | - name: Assert wikmd status 97 | run: curl -I localhost:5000 2>&1 | awk '/HTTP\// {print $2}' | grep -w "200\|301" 98 | 99 | # Check that wikmd is rendering 100 | - name: Check wikmd rendering status 101 | run: curl -s localhost:5000 | grep -w "What is it?" 102 | 103 | publish: 104 | # Publish if official repo and push to 'main' or new tag 105 | if: | 106 | github.repository == 'Linbreux/wikmd' && 107 | (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) 108 | runs-on: ubuntu-latest 109 | needs: [build, test] 110 | steps: 111 | - name: Set up Docker Buildx 112 | uses: docker/setup-buildx-action@v2 113 | 114 | - name: Login to DockerHub 115 | uses: docker/login-action@v2 116 | with: 117 | username: ${{ secrets.DOCKERHUB_USERNAME }} 118 | password: ${{ secrets.DOCKERHUB_TOKEN }} 119 | 120 | - name: Publish 121 | uses: docker/build-push-action@v3 122 | with: 123 | file: Dockerfile 124 | push: true 125 | tags: ${{ needs.build.outputs.wikmd_tags }} 126 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.8', '3.9', '3.10'] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install pandoc 20 | run: sudo apt-get update && sudo apt-get install pandoc 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install .[dev] 25 | - name: Test with pytest 26 | run: | 27 | python -m pytest 28 | 29 | - name: Start wikmd 30 | run: | 31 | cd src 32 | python -m wikmd.wiki & 33 | 34 | - name: screenshots-ci-action 35 | uses: flameddd/screenshots-ci-action@master 36 | with: 37 | url: http://localhost:5000 38 | 39 | - uses: actions/upload-artifact@v3 # Uplaod screenshots to Actions Artifacts via actions/upload-artifact@v2 40 | with: 41 | path: screenshots 42 | name: Download-screenshots 43 | -------------------------------------------------------------------------------- /.github/workflows/publish-pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PIPY_KEY }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # pycharm IDE config folder 2 | .idea/ 3 | 4 | # Log files 5 | *.log 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # PyBuilder 65 | .pybuilder/ 66 | target/ 67 | 68 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 69 | __pypackages__/ 70 | 71 | # Virtual Environments 72 | .env 73 | .venv 74 | env/ 75 | venv/ 76 | ENV/ 77 | 78 | # Spyder project settings 79 | .spyderproject 80 | .spyproject 81 | 82 | # mkdocs documentation 83 | /site 84 | 85 | # mypy 86 | .mypy_cache/ 87 | .dmypy.json 88 | dmypy.json 89 | 90 | # Pyre type checker 91 | .pyre/ 92 | 93 | # pytype static type analyzer 94 | .pytype/ 95 | 96 | # Cython debug symbols 97 | cython_debug/ 98 | 99 | # Generated plugin files 100 | src/wikmd/plugins/draw/drawings/* 101 | !src/wikmd/plugins/draw/drawings/.gitkeep 102 | 103 | 104 | # Personal wiki pages 105 | wiki/* 106 | 107 | temp/* 108 | -------------------------------------------------------------------------------- /DOCKER.md: -------------------------------------------------------------------------------- 1 | # wikmd Docker image 2 | 3 | [wikmd](https://github.com/Linbreux/wikmd) is a file based wiki that uses markdown. 4 | 5 | ## Usage 6 | 7 | Here are some example snippets to help you get started creating a container. 8 | 9 | Build the image, 10 | 11 | ```bash 12 | docker build -t linbreux/wikmd:latest -f Dockerfile . 13 | ``` 14 | 15 | ### docker-compose (recommended, [click here for more info](https://docs.linuxserver.io/general/docker-compose)) 16 | 17 | ```yaml 18 | --- 19 | version: "2.1" 20 | services: 21 | wikmd: 22 | image: linbreux/wikmd:latest 23 | container_name: wikmd 24 | environment: 25 | - PUID=1000 26 | - PGID=1000 27 | - TZ=Europe/Paris 28 | - HOMEPAGE=homepage.md #optional 29 | - HOMEPAGE_TITLE=homepage.md #optional 30 | - WIKMD_LOGGING=1 #optional 31 | volumes: 32 | - /path/to/wiki:/wiki 33 | ports: 34 | - 5000:5000 35 | restart: unless-stopped 36 | ``` 37 | 38 | ### docker cli ([click here for more info](https://docs.docker.com/engine/reference/commandline/cli/)) 39 | 40 | ```bash 41 | docker run -d \ 42 | --name wikmd \ 43 | -e TZ=Europe/Paris \ 44 | -e PUID=1000 \ 45 | -e PGID=1000 \ 46 | -e HOMEPAGE=homepage.md `#optional` \ 47 | -e HOMEPAGE_TITLE=homepage.md `#optional` \ 48 | -e WIKMD_LOGGING=1 `#optional` \ 49 | -p 5000:5000 \ 50 | -v /path/to/wiki:/wiki \ 51 | --restart unless-stopped \ 52 | linbreux/wikmd:latest 53 | ``` 54 | 55 | ## Parameters 56 | 57 | Container images are configured using parameters passed at runtime (such as those above). These parameters are separated by a colon and indicate `:` respectively. For example, `-p 5000:5000` would expose port `5000` from inside the container to be accessible from the host's IP on port `5000` outside the container. 58 | 59 | | Parameter | Function | 60 | | :----: | --- | 61 | | `-p 5000` | Port for wikmd webinterface. | 62 | | `-e PUID=1000` | for UserID - see below for explanation | 63 | | `-e PGID=1000` | for GroupID - see below for explanation | 64 | | `-e TZ=Europe/Paris` | Specify a timezone to use EG Europe/Paris | 65 | | `-e HOMEPAGE=homepage.md` | Specify the file to use as a homepage | 66 | | `-e HOMEPAGE_TITLE=title` | Specify the homepage's title | 67 | | `-e WIKMD_LOGGING=1` | Enable/disable file logging | 68 | | `-v /wiki` | Path to the file-based wiki. | 69 | 70 | ## User / Group Identifiers 71 | 72 | When using volumes (`-v` flags) permissions issues can arise between the host OS and the container, we avoid this issue by allowing you to specify the user `PUID` and group `PGID`. 73 | 74 | Ensure any volume directories on the host are owned by the same user you specify and any permissions issues will vanish like magic. 75 | 76 | In this instance `PUID=1000` and `PGID=1000`, to find yours use `id user` as below: 77 | 78 | ```bash 79 | $ id username 80 | uid=1000(dockeruser) gid=1000(dockergroup) groups=1000(dockergroup) 81 | ``` 82 | 83 | ## Support Info 84 | 85 | * Shell access whilst the container is running: `docker exec -it wikmd /bin/bash` 86 | * To monitor the logs of the container in realtime: `docker logs -f wikmd` 87 | 88 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-alpine3.17 as python-base 2 | 3 | # Prevents Python from writing pyc files. 4 | ENV PYTHONDONTWRITEBYTECODE=1 5 | 6 | # Keeps Python from buffering stdout and stderr to avoid situations where 7 | # the application crashes without emitting any logs due to buffering. 8 | ENV PYTHONUNBUFFERED=1 9 | 10 | # We will be installing venv 11 | ENV VIRTUAL_ENV="/venv" 12 | 13 | # Create a non-privileged user that the app will run under. 14 | # See https://docs.docker.com/go/dockerfile-user-best-practices/ 15 | ARG UID=10001 16 | RUN adduser \ 17 | --disabled-password \ 18 | --gecos "" \ 19 | --home "/nonexistent" \ 20 | --shell "/sbin/nologin" \ 21 | --no-create-home \ 22 | --uid "${UID}" \ 23 | appuser 24 | 25 | 26 | # Add project path to python path, this to ensure we can reach it from anywhere 27 | WORKDIR /code 28 | ENV PYTHONPATH="/code:$PYTHONPATH" 29 | 30 | # prepare the virtual env 31 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 32 | RUN python -m venv $VIRTUAL_ENV 33 | 34 | # BUILDER 35 | FROM python-base as python-builder 36 | 37 | # Install our dependencies 38 | RUN apk update 39 | RUN apk add git 40 | RUN apk add build-base linux-headers 41 | 42 | # Python dependencies 43 | WORKDIR /code 44 | 45 | COPY pyproject.toml /code 46 | 47 | # Copy the py project and use a package to convert our pyproject.toml file into a requirements file 48 | # We can not install the pyproject with pip as that would install the project and we only 49 | # wants to install the project dependencies. 50 | RUN python -m pip install toml-to-requirements==0.2.0 51 | RUN toml-to-req --toml-file pyproject.toml 52 | 53 | RUN python -m pip install --no-cache-dir --upgrade -r ./requirements.txt 54 | 55 | # Copy our source content over 56 | COPY ./src/wikmd /code/wikmd 57 | 58 | # Change the directory to the root. 59 | WORKDIR / 60 | 61 | # Expose the port that the application listens on. 62 | EXPOSE 5000 63 | 64 | # Run the application. 65 | CMD ["python", "-m", "wikmd.wiki"] 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 linbreux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wikmd 2 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/linbreux/wikmd/docker.yml?style=flat-square) ![GitHub Repo stars](https://img.shields.io/github/stars/linbreux/wikmd?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/linbreux/wikmd?style=flat-square) ![GitLab](https://img.shields.io/gitlab/license/linbreux/wikmd?style=flat-square) 3 | > :information_source: **Information** can be found in the docs: [https://linbreux.github.io/wikmd/](https://linbreux.github.io/wikmd/) 4 | 5 | ## What is it? 6 | It’s a file-based wiki that aims to simplicity. Instead of storing the data in a database I chose to have a file-based system. The advantage of this system is that every file is directly readable inside a terminal etc. Also when you have direct access to the system you can export the files to anything you like. 7 | 8 | To view the documents in the browser, the document is converted to html. 9 | 10 | ![preview](src/wikmd/static/images/readme-img.png) 11 | 12 | ## Features 13 | 14 | - knowledge graph 15 | - **NEW** plugin system: drawio integration, alerts, embedded pages, swagger, plantuml 16 | - git support (version control) 17 | - image support including sizing and referencing 18 | - math/latex 19 | - code highlight 20 | - file searching 21 | - file based 22 | - dark theme 23 | - codemirror for editing 24 | - password protection 25 | 26 | 27 | [Using the wiki](https://linbreux.github.io/wikmd/Using%20the%20wiki.html) 28 | 29 | 30 | ## Installation 31 | 32 | Detailed installation instruction can be found [here](https://linbreux.github.io/wikmd/installation.html). 33 | 34 | ## Development 35 | 36 | Instructions on the easiest way to develop on the project can be found [here](https://linbreux.github.io/wikmd/development.html). 37 | 38 | ## Plugins & Knowledge graph (beta) 39 | 40 | More info can be found in the [docs](https://linbreux.github.io/wikmd/knowledge%20graph.html). 41 | 42 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | - [ ] Fix docker problem when using git 3 | - [ ] Invert knowledge graph. (linked to pages should be large) 4 | -------------------------------------------------------------------------------- /docs/Customization.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Customization 4 | nav_order: 9 5 | --- 6 | 7 | # Change default colors 8 | 9 | It's possible to change the default colors of the wiki by customizing `wiki.colors.css`. This file can be found in 10 | 11 | ``` 12 | static/css/wiki.colors.css 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/Screenshots.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Screenshots 4 | nav_order: 3 5 | --- 6 | 7 | # Screenshots 8 | 9 | ## Homepage 10 | 11 | ![homepage](images/homepage.png) 12 | 13 | ## List all files 14 | 15 | ![list](images/list.png) 16 | 17 | ## Editor 18 | 19 | ![editor](images/editor.png) 20 | 21 | ## File upload 22 | 23 | ![file upload](images/file_upload.png) 24 | 25 | ## Graph 26 | 27 | ![graph](images/graph.png) 28 | 29 | ## Dark theme 30 | 31 | ![dark theme](images/black.png) 32 | 33 | ![light theme](images/lightmode.png) 34 | 35 | ## Password protect 36 | 37 | ![password protect](images/password_protect.png) 38 | -------------------------------------------------------------------------------- /docs/Using the wiki.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Using the wiki 4 | nav_order: 7 5 | --- 6 | # Using the wiki 7 | 8 | ## Latex 9 | 10 | It's possible to use latex syntax inside your markdown because the markdown is first converted to latex and after that to html. This means you have a lot more flexibility. 11 | 12 | ### Change image size 13 | 14 | ``` 15 | ![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} 16 | ``` 17 | 18 | ### Image references 19 | ``` 20 | ![Nice mountain](https://i.ibb.co/Dzp0SfC/download.jpg){#fig:mountain width="50%"} 21 | 22 | Inside picture @fig:mountain you can see a nice mountain. 23 | 24 | ``` 25 | 26 | ### Math 27 | ``` 28 | \begin{align} 29 | y(x) &= \int_0^\infty x^{2n} e^{-a x^2}\,dx\\ 30 | &= \frac{2n-1}{2a} \int_0^\infty x^{2(n-1)} e^{-a x^2}\,dx\\ 31 | &= \frac{(2n-1)!!}{2^{n+1}} \sqrt{\frac{\pi}{a^{2n+1}}}\\ 32 | &= \frac{(2n)!}{n! 2^{2n+1}} \sqrt{\frac{\pi}{a^{2n+1}}} 33 | \end{align} 34 | ``` 35 | 36 | ``` 37 | You can also use $inline$ math to show $a=2$ and $b=8$ 38 | ``` 39 | 40 | ## Converting the files 41 | 42 | Open the wiki folder of your instance. 43 | 44 | |- static 45 | |- templates 46 | |- **wiki** <--This folder 47 | |- wiki.py 48 | 49 | In this folder all the markdownfiles are listed. Editing the files will be visible in the web-version. 50 | 51 | |- homepage.md 52 | |- How to use the wiki.md 53 | |- Markdown cheatsheet.md 54 | 55 | The advantage is that u can use the commandline to process some data. For example using pandoc: 56 | ``` 57 | pandoc -f markdown -t latex homepage.md How\ to\ use\ the\ wiki.md -o file.pdf --pdf-engine=xelatex 58 | ``` 59 | This creates a nice pdf version of your article. Its possible you have to create a yml header on top of your document to set for example the margins better. 60 | ``` 61 | --- 62 | title: titlepage 63 | author: your name 64 | date: 05-11-2020 65 | geometry: margin=2.5cm 66 | header-includes: | 67 | \usepackage{caption} 68 | \usepackage{subcaption} 69 | lof: true 70 | --- 71 | ``` 72 | For more information you have to read the pandoc documentation. 73 | 74 | ### New features 75 | Create a page with a random id by typing *{id}* in the name. 76 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pmarsceill/just-the-docs 2 | -------------------------------------------------------------------------------- /docs/connect_git_and_online_repo.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Connect a online git repo 4 | nav_order: 8 5 | --- 6 | 7 | It's possible to connect Wikmd with an online repo. In this example we will give an example with github. 8 | 9 | # Create a new repo on Github 10 | 11 | ![create notes repo on Github](images/github_create_notes.png) 12 | 13 | Copy the givin ssh-key, for example: 14 | 15 | ``` 16 | git@github.com:Linbreux/notes.git 17 | ``` 18 | 19 | Go in the wikmd folder to ```wikmd-config.yaml``` and change the following lines 20 | 21 | ``` 22 | sync_with_remote: 1 23 | remote_url: "git@github.com:Linbreux/notes.git" 24 | ``` 25 | 26 | When you add or change a file to your wiki, the repo will be synced. 27 | 28 | > Make sure to register your github [ssh-keys](https://docs.github.com/en/authentication/connecting-to-github-with-ssh) 29 | 30 | # Connect existing Github repo 31 | 32 | Copy the givin ssh-key, for example: 33 | 34 | ``` 35 | git@github.com:Linbreux/notes.git 36 | ``` 37 | 38 | Go in the wikmd folder to ```wikmd-config.yaml``` and change the following lines 39 | 40 | ``` 41 | sync_with_remote: 1 42 | remote_url: "git@github.com:Linbreux/notes.git" 43 | ``` 44 | 45 | When you add or change a file to your wiki, the repo will be synced. Duplicated files from the local and remote repo will be renamed to ```--copy.md``` 46 | 47 | > Make sure to register your github [ssh-keys](https://docs.github.com/en/authentication/connecting-to-github-with-ssh) -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Development 4 | parent: Installation 5 | nav_order: 12 6 | --- 7 | 8 | # Regular installation 9 | ! It's tested on windows and linux based systems. 10 | ! Runs on flask server 11 | 12 | Clone the repository 13 | ``` 14 | git clone https://github.com/Linbreux/wikmd.git 15 | ``` 16 | 17 | cd in wikmd 18 | ``` 19 | cd wikmd 20 | ``` 21 | 22 | Create a virtual env and activate it (optional, but highly recommended) 23 | ``` 24 | virtualenv venv 25 | source venv/bin/activate 26 | ``` 27 | 28 | Install it in [development mode aka editable install](https://setuptools.pypa.io/en/latest/userguide/development_mode.html) 29 | ``` 30 | bash: python -m pip install --editable .[dev] 31 | zsh: python -m pip install --editable '.[dev]' 32 | ``` 33 | 34 | Run the wiki 35 | ``` 36 | python -m wikmd.wiki 37 | ``` 38 | 39 | ## Adding a plugin 40 | 41 | Add the plugin to the `plugins` folder and add the `foldername` to section `plugins` in the `wikmd-config.yaml` file. 42 | 43 | ## Construction 44 | 45 | Plugins are listed inside the `plugins` folder. 46 | 47 | ``` 48 | plugins/ 49 | ├─ plugin1/ 50 | │ ├─ plugin1.py 51 | │ ├─ ... 52 | ├─ plugin2/ 53 | │ ├─ plugin2.py 54 | │ ├─ ... 55 | ├─ .../ 56 | ``` 57 | 58 | The name of the plugin should be the same as the folder. Inside the python file should be a `Plugin` class, this is the class that will be loaded into the python program. 59 | 60 | ### Methods 61 | 62 | For now there are only a few supported methods that can be added to the `Plugin` class. Feel free to extend them! 63 | 64 | #### get_plugin_name() -> str *required* 65 | 66 | This method should return the name of the plugin. 67 | 68 | #### process_md(md: str) -> str *optional* 69 | 70 | This method will be called before saving the markdown file. The returned string is the content of the saved file. 71 | 72 | #### process_md_before_html_convert(md: str) -> str *optional* 73 | 74 | This method will be called before converting markdown file into html and after saving the markdown file. All changes 75 | made in this method are not going to be saved in .md file, but will be shown on the html page. 76 | 77 | #### process_html(md: str) -> str *optional* 78 | 79 | This method will be called before showing the html page. The returned string is the content of the html file that will be shown. 80 | 81 | #### communicate_plugin(request) -> str *optional* 82 | 83 | The parameter `request` is the `POST` request thats returned by `/plug_com` (plugin communication). 84 | 85 | -------------------------------------------------------------------------------- /docs/environment variables.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Configuration with environment variables 4 | nav_order: 5 5 | --- 6 | # Configuration with environment variables 7 | 8 | You can change static locations using Environment variables. Using (in linux): 9 | ``` 10 | export = 11 | ``` 12 | 13 | ## Homepage 14 | 15 | The homepage is defaulted to the "homepage.md" file. If you want to use a different file, 16 | you can specify the file to use for the homepage with the environment variable `HOMEPAGE` as well as the title with `HOMEPAGE_TITLE`. 17 | 18 | ``` 19 | export HOMEPAGE= 20 | export HOMEPAGE_TITLE= 21 | ``` 22 | 23 | ## Wiki title 24 | 25 | The title/name is displayed in the top left corner of the GUI and in the browser tab. It is defaulted to "Wiki". Set this to a unique name to tell multiple instances of wikmd apart. 26 | 27 | ``` 28 | export WIKI_TITLE="My wiki" 29 | ``` 30 | 31 | ## Custom port and host 32 | 33 | You can change the host and port value by respectively changing the `WIKMD_HOST` and `WIKMD_PORT` variable. 34 | 35 | ``` 36 | export WIKMD_HOST=0.0.0.0 37 | export WIKMD_PORT=80 38 | ``` 39 | 40 | 41 | ## Custom data path 42 | 43 | Usually, wikmd looks for content in the subfolder `wiki`. In case you want to store your wiki data somewhere else, you 44 | can set a custom data path via the environment variable `WIKI_DIRECTORY`: 45 | 46 | ``` 47 | export WIKI_DIRECTORY="~/.wikidata" 48 | ``` 49 | 50 | ## Custom picture upload 51 | 52 | It's possible to add a custom picture upload path. This is done by adding a `IMAGE_ROUTE` environment variable. 53 | 54 | ``` 55 | export IMAGES_ROUTE="media/pictures" 56 | ``` 57 | 58 | ## Password protect 59 | 60 | It's possible to password protect changing and removing of files. This can be done using the following parameters: 61 | 62 | ``` 63 | export PROTECT_EDIT_BY_PASSWORD = 1 64 | export PASSWORD_IN_SHA_256 = <your password in sha 256> 65 | ``` 66 | 67 | You can generate it via the console or just us a website (ex.[https://emn178.github.io/online-tools/sha256.html](https://emn178.github.io/online-tools/sha256.html)). 68 | 69 | ## Local Mode 70 | 71 | If enabled wikmd will serve all `css` and `js` files itself. 72 | Otherwise the CDNs jsdelivr, cloudflare, polyfill and unpkg will be used. 73 | 74 | `Default = False` 75 | 76 | ``` 77 | export LOCAL_MODE=True 78 | ``` 79 | 80 | ## Optimize Images 81 | 82 | If enabled optimizes images by converting them to webp files. 83 | Allowed values are `no`, `lossless` and `lossy`. 84 | 85 | | | `lossless` | `lossy` | 86 | |-----|-----------------|----------| 87 | | gif | lossless | lossless | 88 | | jpg | _near_ lossless | lossy | 89 | | png | lossless | lossless | 90 | 91 | `Default = "no"` 92 | 93 | ``` 94 | export OPTIMIZE_IMAGES="lossy" 95 | ``` 96 | 97 | ### How to install webp 98 | You need to have the programs `cwebp` and `gif2webp` installed to use this feature. 99 | Everyone not listed below has to get the binaries themselves: https://developers.google.com/speed/webp/docs/precompiled 100 | 101 | | Operating System | How to install | 102 | |------------------|--------------------------------| 103 | | Arch & Manjaro | `pacman -S libwebp` | 104 | | Alpine | `apk add libwebp-tools` | 105 | | Debian & Ubuntu | `apt install webp` | 106 | | Fedora | `dnf install libwebp-tools` | 107 | | macOS homebrew | `brew install webp` | 108 | | macOS MacPorts | `port install webp` | 109 | | OpenSuse | `zypper install libwebp-tools` | 110 | 111 | ## Caching 112 | 113 | By default wikmd will cache wiki pages to `/dev/shm/wikmd/cache`, changing this option changes 114 | the directory that cached files will be stored in. 115 | 116 | Do not change this location to be within your Markdown documents directory. 117 | 118 | `Default = "/dev/shm/wikmd/cache"` 119 | 120 | ``` 121 | export CACHE_DIR="/some/other/path" 122 | ``` 123 | 124 | ## Search index location 125 | By default wikmd will store its search index in `/dev/shm/wikmd/searchindex`, changing this option changes 126 | the directory that the search index will be stored in. 127 | 128 | Do not change this location to be within your Markdown documents directory. 129 | 130 | `Default = "/dev/shm/wikmd/searchindex"` 131 | 132 | ``` 133 | export SEARCH_DIR="/some/other/path" 134 | ``` 135 | 136 | ## Change logging file 137 | 138 | In case you need to rename the log file you can use `WIKMD_LOGGING_FILE`. 139 | 140 | `Default = wikmd.log` 141 | 142 | ``` 143 | export WIKMD_LOGGING_FILE=custom_log.log 144 | ``` 145 | 146 | ## Disable logging 147 | 148 | You could optionaly choose to disable logging by setting the environment variable `WIKMD_LOGGING` to `0`. 149 | 150 | `Default = 1` 151 | 152 | ``` 153 | export WIKMD_LOGGING=0 154 | ``` 155 | 156 | ## Enable synchronization with remote repo 157 | 158 | You could specify if you want to synchronize the wiki with your personal remote git repo. 159 | 160 | To do this, set the environment variable `SYNC_WITH_REMOTE` to `1`. 161 | 162 | 163 | ``` 164 | export SYNC_WITH_REMOTE=1 165 | ``` 166 | 167 | Also set the environment variable `REMOTE_URL` to your remote repo URL. 168 | 169 | 170 | ``` 171 | export REMOTE_URL="https://github.com/user/wiki_repo.git" 172 | ``` 173 | 174 | ## Custom git user and email 175 | 176 | If you want to use custom git user and email, set the environment variables `GIT_USER` and `GIT_EMAIL`. 177 | 178 | The default user is `wikmd` and the email is `wikmd@no-mail.com`. 179 | 180 | ``` 181 | export GIT_USER="your_user" 182 | export GIT_EMAIL="your_email@domain.com" 183 | ``` 184 | 185 | ## Custom main branch name 186 | 187 | You can specify a custom name for the main branch of the wiki repo setting the `MAIN_BRANCH_NAME` environment variable. 188 | The default value is the new standard `main`, but a common older choice is `master`. 189 | 190 | ``` 191 | export MAIN_BRANCH_NAME="master" 192 | ``` 193 | -------------------------------------------------------------------------------- /docs/images/__EMPTY__: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/images/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/docs/images/black.png -------------------------------------------------------------------------------- /docs/images/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/docs/images/editor.png -------------------------------------------------------------------------------- /docs/images/file_upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/docs/images/file_upload.png -------------------------------------------------------------------------------- /docs/images/github_create_notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/docs/images/github_create_notes.png -------------------------------------------------------------------------------- /docs/images/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/docs/images/graph.png -------------------------------------------------------------------------------- /docs/images/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/docs/images/homepage.png -------------------------------------------------------------------------------- /docs/images/lightmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/docs/images/lightmode.png -------------------------------------------------------------------------------- /docs/images/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/docs/images/list.png -------------------------------------------------------------------------------- /docs/images/password_protect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/docs/images/password_protect.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Home 4 | nav_order: 1 5 | --- 6 | 7 | # wikmd 8 | ![preview](images/homepage.png) 9 | 10 | ## Features 11 | - git support (version control) 12 | - image support including sizing and referencing 13 | - math/latex 14 | - code highlight 15 | - file searching 16 | - file based 17 | - dark theme 18 | - codemirror for editing 19 | - knowledge graph 20 | - basic password protection 21 | 22 | ## What is it? 23 | It’s a file-based wiki that aims to simplicity. The documents are completely written in Markdown which is an easy markup language that you can learn in 60 sec. 24 | 25 | ## How does it work? 26 | Instead of storing the data in a database I chose to have a file-based system. The advantage of this system is that every file is directly readable inside a terminal etc. Also when you have direct access to the system you can export the files to anything you like. 27 | 28 | To view the documents in the browser, the document is converted to html. 29 | 30 | [Installation](installation.md) 31 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Installation 4 | nav_order: 2 5 | has_children: true 6 | --- 7 | 8 | # Installation 9 | 10 | For now it's possible to install Wikmd via a [git clone](installation/regular_install) or via [Docker](installation/docker). 11 | -------------------------------------------------------------------------------- /docs/installation/docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Docker installation 4 | parent: Installation 5 | nav_order: 2 6 | --- 7 | 8 | ## Usage 9 | 10 | Here are some example snippets to help you get started creating a container. 11 | 12 | Pull down [the latest image from Docker Hub](https://hub.docker.com/r/linbreux/wikmd): 13 | 14 | ```bash 15 | docker pull linbreux/wikmd 16 | ``` 17 | 18 | Or, build the image after cloning the source code itself: 19 | 20 | ```bash 21 | git clone https://github.com/linbreux/wikmd.git && cd wikmd 22 | docker build -t linbreux/wikmd:latest -f Dockerfile . 23 | ``` 24 | 25 | ### docker-compose (recommended, [click here for more info](https://docs.linuxserver.io/general/docker-compose)) 26 | 27 | ```yaml 28 | --- 29 | version: "2.1" 30 | services: 31 | wikmd: 32 | image: linbreux/wikmd:latest 33 | container_name: wikmd 34 | environment: 35 | - PUID=1000 36 | - PGID=1000 37 | - TZ=Europe/Paris 38 | volumes: 39 | - /path/to/wiki:/wiki 40 | ports: 41 | - 5000:5000 42 | restart: unless-stopped 43 | ``` 44 | 45 | ### docker cli ([click here for more info](https://docs.docker.com/engine/reference/commandline/cli/)) 46 | 47 | ```bash 48 | docker run -d \ 49 | --name wikmd \ 50 | -e TZ=Europe/Paris \ 51 | -e PUID=1000 \ 52 | -e PGID=1000 \ 53 | -e WIKMD_LOGGING=1 `#optional` \ 54 | -p 5000:5000 \ 55 | -v /path/to/wiki:/wiki \ 56 | --restart unless-stopped \ 57 | linbreux/wikmd:latest 58 | ``` 59 | 60 | ## Parameters 61 | 62 | Container images are configured using parameters passed at runtime (such as those above). These parameters are separated by a colon and indicate `<external>:<internal>` respectively. For example, `-p 5000:5000` would expose port `5000` from inside the container to be accessible from the host's IP on port `5000` outside the container. 63 | 64 | | Parameter | Function | 65 | |:--------------------------|-------------------------------------------| 66 | | `-p 5000` | Port for wikmd webinterface. | 67 | | `-e PUID=1000` | for UserID - see below for explanation | 68 | | `-e PGID=1000` | for GroupID - see below for explanation | 69 | | `-e TZ=Europe/Paris` | Specify a timezone to use EG Europe/Paris | 70 | | `-e HOMEPAGE=homepage.md` | Specify the file to use as a homepage | 71 | | `-e HOMEPAGE_TITLE=title` | Specify the homepage's title | 72 | | `-e WIKMD_LOGGING=1` | Enable/disable file logging | 73 | | `-v /wiki` | Path to the file-based wiki. | 74 | 75 | ## User / Group Identifiers 76 | 77 | When using volumes (`-v` flags) permissions issues can arise between the host OS and the container, we avoid this issue by allowing you to specify the user `PUID` and group `PGID`. 78 | 79 | Ensure any volume directories on the host are owned by the same user you specify and any permissions issues will vanish like magic. 80 | 81 | In this instance `PUID=1000` and `PGID=1000`, to find yours use `id user` as below: 82 | 83 | ```bash 84 | $ id username 85 | uid=1000(dockeruser) gid=1000(dockergroup) groups=1000(dockergroup) 86 | ``` 87 | 88 | ## Support Info 89 | 90 | * Shell access whilst the container is running: `docker exec -it wikmd /bin/bash` 91 | * To monitor the logs of the container in realtime: `docker logs -f wikmd` 92 | 93 | -------------------------------------------------------------------------------- /docs/installation/regular_install.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Regular installation 4 | parent: Installation 5 | nav_order: 1 6 | --- 7 | 8 | # Fast installation via pip 9 | 10 | Wikmd is available as a [pip package](https://pypi.org/project/wikmd/). 11 | ``` 12 | pip install wikmd 13 | ``` 14 | Now `wikmd` is available as a console command. It will create or use a wiki folder in the current working directory. 15 | 16 | # Regular installation 17 | ! It's tested on windows and linux based systems. 18 | ! Runs on flask server 19 | 20 | Clone the repository 21 | ``` 22 | git clone https://github.com/Linbreux/wikmd.git 23 | ``` 24 | cd in wikmd 25 | ``` 26 | cd wikmd 27 | ``` 28 | 29 | Create a virtual env and activate it (optional, but highly recommended) 30 | ``` 31 | virtualenv venv 32 | source venv/bin/activate 33 | ``` 34 | 35 | Install 36 | ``` 37 | python -m pip install . 38 | ``` 39 | 40 | Run the wiki, remember that all paths within wikmd are defined as relative. 41 | That means that your current working directory will be the base of the project. 42 | As such your current directory will hold the `wiki` directory that contains all md files. 43 | ``` 44 | python -m wikmd.wiki 45 | ``` 46 | 47 | Now visit localhost:5000 and you will see the wiki. With the 0.0.0.0. option it will show up everywhere on the network. 48 | 49 | If there are problems feel free to open an issue on github. You can paste the output of `wikmd.log` in the issue. 50 | 51 | ## Linux 52 | 53 | Maybe you need to install pandoc on your system before this works. 54 | ``` 55 | sudo apt-get update && sudo apt-get install pandoc 56 | ``` 57 | 58 | You may experience an issue when running `python -m pip install -r requirements.txt` where you receive the following error: 59 | ``` 60 | psutil/_psutil_common.c:9:10: fatal error: Python.h: No such file or directory 61 | 9 | #include <Python.h> 62 | | ^~~~~~~~~~ 63 | compilation terminated. 64 | error: command '/usr/lib64/ccache/gcc' failed with exit code 1 65 | ---------------------------------------- 66 | ERROR: Failed building wheel for psutil 67 | ``` 68 | 69 | You can fix this by installing the python 3 dev package. 70 | 71 | Ubuntu, Debian: 72 | ``` 73 | sudo apt-get install python3-dev 74 | ``` 75 | Fedora: 76 | ``` 77 | sudo dnf install python3-devel 78 | ``` 79 | For other distros, you can search up `[distro] install python 3 dev`. 80 | 81 | You may experience an error when running `python pip install .` where it asks you to install `gcc python3-dev`. Example: 82 | ``` 83 | unable to execute 'x86_64-linux-gnu-gcc': No such file or directory 84 | C compiler or Python headers are not installed on this system. Try to run: 85 | sudo apt-get install gcc python3-dev 86 | error: command 'x86_64-linux-gnu-gcc' failed with exit status 1 87 | ---------------------------------------- 88 | ERROR: Failed building wheel for psutil 89 | ``` 90 | 91 | Simply install `gcc python3-dev`. 92 | 93 | 94 | ### Runing the wiki as a service 95 | 96 | You can run the wiki as a service. Doing this will allow the wiki to boot at startup. 97 | 98 | First, create the following file as `wiki.service` in `/etc/systemd/system`, and replace the placeholder entries. 99 | 100 | ``` 101 | [Unit] 102 | Description=Wikmd 103 | After=network.target 104 | 105 | [Service] 106 | User=<user> 107 | WorkingDirectory=<path to the wiki> 108 | ExecStart=<path to the wiki>/env/bin/python3 -m wikmd.wiki 109 | Restart=always 110 | 111 | [Install] 112 | WantedBy=multi-user.target 113 | 114 | ``` 115 | 116 | Run the following commands to enable and start the serivce 117 | 118 | ``` 119 | systemctl daemon-reload 120 | systemctl enable wiki.service 121 | systemctl start wiki.service 122 | ``` 123 | 124 | If the wiki opens, but does not display any text, run `systemctl status wiki.service`. 125 | 126 | You may see the following error: 127 | ``` 128 | ERROR in wiki: Conversion to HTML failed >>> Pandoc died with exitcode "83" during conversion: b'Error running filter pandoc-xnos:\nCould not find executable pandoc-xnos\n' 129 | ``` 130 | To fix, run the following commands: 131 | ``` 132 | sudo su 133 | cd ~ 134 | umask 022 135 | pip install -r <path to the wiki> . 136 | ``` 137 | This will install the python packages system-wide, allowing the wiki service to access it. 138 | 139 | Run `systemctl restart wiki.service` and it should be working. 140 | 141 | 142 | ## Windows 143 | 144 | You should install [pandoc](https://pandoc.org/installing.html) on your windows system. Now you should be able to start 145 | the server. 146 | ``` 147 | python -m wikmd.wiki 148 | ``` 149 | If the content of the markdown files are not visible you should add the `pandoc-xnos` location to your path variable. Info about [Environment variables](https://www.computerhope.com/issues/ch000549.htm). 150 | ``` 151 | pip show --files pandoc-xnos 152 | # look for "location" in the output 153 | # example: C:\users\<user>\appdata\local\packages\pythonsoftwarefoundation.python.3.x\localcache\local-packages\python38\site-packages 154 | # change "site-packages" to "Scripts" 155 | # example: C:\Users\<user>\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.x\LocalCache\local-packages\Python38\Scripts 156 | # open your Environment variables and add the changed line to the path 157 | SET PATH=%PATH%;C:\Users\<user>\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.x\LocalCache\local-packages\Python38\Scripts 158 | ``` 159 | -------------------------------------------------------------------------------- /docs/knowledge graph.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Knowledge graph 4 | nav_order: 4 5 | --- 6 | 7 | # Knowledge graph (beta) 8 | A knowledge graph that visualize the links between different pages. 9 | 10 | ![Knowledge graph](images/graph.png) 11 | 12 | ## Usage 13 | 14 | Link files with a full path name like: ```/test/path``` instead of ```path```. The graph only shows files inside the wiki, external links are ignored. 15 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Plugins 4 | nav_order: 10 5 | --- 6 | 7 | # Plugins 8 | 9 | The plugin system is still in **beta** for now. 10 | 11 | ## Supported Plugins 12 | 13 | The plugins are used to extend the functionality of the wiki. Most of them are accessible through the use of `tags`. 14 | For now there are only a few supported. 15 | 16 | ### Drawio plugin 17 | Allows you to add an **interactive drawio drawing** to the wiki. Use `[[draw]]` 18 | tag to insert a drawio block, that can be edited in page preview mode. 19 | 20 | ### Alerts 21 | Allows to insert an alert message in the page text. Here is a list of 22 | possible alert messages: 23 | - `[[info]]` 24 | - `[[warning]]` 25 | - `[[danger]]` 26 | - `[[success]]` 27 | 28 | ### Embedded pages 29 | Allows to show another page in the current one.<br> Usage:<br>`[[page: some-page]]`<br> where `some-page` 30 | is the name of another page from the wiki 31 | 32 | ### Swagger integration 33 | Allows to insert a **swagger** block into the wiki page. <br> Usage: <br> 34 | `[[swagger link]]` 35 | <br> 36 | where `link` is a link to a GET endpoint with .json openapi file. 37 | <br> 38 | `[[swagger https://petstore3.swagger.io/api/v3/openapi.json]]` can be used as an example. 39 | 40 | ### Plantuml diagrams 41 | Allows to embed a plantuml diagram. 42 | <br>Usage:<br> 43 | `````` 44 | ```plantuml 45 | @startuml 46 | Alice -> Bob: Authentication Request 47 | Bob --> Alice: Authentication Response 48 | 49 | Alice -> Bob: Another authentication Request 50 | Alice <-- Bob: Another authentication Response 51 | @enduml 52 | ``` 53 | `````` 54 | 55 | A custom plantuml server can be defined using configuration file. 56 | Read more about plantuml [here](https://plantuml.com). 57 | 58 | ### Mermaid diagrams 59 | Allows to embed a mermaid diagram. 60 | <br>Usage:<br> 61 | `````` 62 | ```mermaid 63 | graph LR 64 | A[Square Rect] -- Link text --> B((Circle)) 65 | A --> C(Round Rect) 66 | B --> D{Rhombus} 67 | C --> D 68 | ``` 69 | `````` 70 | 71 | Read more about mermaid diagrams [here](https://mermaid.js.org/intro/). 72 | -------------------------------------------------------------------------------- /docs/wikmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/docs/wikmd.png -------------------------------------------------------------------------------- /docs/yaml-configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Configuration with YAML 4 | nav_order: 6 5 | --- 6 | # Configuration with YAML 7 | 8 | The entire configuration of the wiki can be done with environment variables but also through the file 9 | `wikmd-config.yaml`. 10 | 11 | Please, notice that if you set up both environment variables and `wikmd-config.yaml`, the environment variables take 12 | precedence. 13 | 14 | ## Configuration parameters 15 | 16 | ```yaml 17 | # wikmd configuration file 18 | 19 | wikmd_host: "0.0.0.0" 20 | wikmd_port: 5000 21 | wikmd_logging: 1 22 | wikmd_logging_file: "wikmd.log" 23 | 24 | git_user: "wikmd" 25 | git_email: "wikmd@no-mail.com" 26 | 27 | main_branch_name: "main" 28 | sync_with_remote: 0 29 | remote_url: "" 30 | 31 | wiki_directory: "wiki" 32 | wiki_title: "Wiki" 33 | homepage: "homepage.md" 34 | homepage_title: "homepage" 35 | images_route: "img" 36 | image_allowed_mime: ["image/gif", "image/jpeg", "image/png", "image/svg+xml", "image/webp"] 37 | 38 | protect_edit_by_password: 0 39 | password_in_sha_256: "0E9C700FAB2D5B03B0581D080E74A2D7428758FC82BD423824C6C11D6A7F155E" #ps: wikmd 40 | 41 | local_mode: false 42 | 43 | # Valid values are "no", "lossless" and "lossy" 44 | optimize_images: "no" 45 | 46 | cache_dir: "/dev/shm/wikmd/cache" 47 | search_dir: "/dev/shm/wikmd/searchindex" 48 | ``` 49 | 50 | Please, refer to [environment variables](environment%20variables.md) for further parameters explanation. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | requires-python = ">= 3.8" 3 | name = "wikmd" 4 | description = "A file-based wiki that aims for simplicity." 5 | readme = "README.md" 6 | authors = [ 7 | { name = "linbreux" } 8 | ] 9 | 10 | version = "1.10.6" 11 | 12 | dependencies=[ 13 | "Flask==3.0.2", 14 | "GitPython==3.1.42", 15 | "Markdown==3.5.2", 16 | "PyYAML==6.0.1", 17 | "Werkzeug==3.0.3", 18 | "Whoosh==2.7.4", 19 | "beautifulsoup4==4.12.3", 20 | "pandoc-eqnos==2.5.0", 21 | "pandoc-fignos==2.4.0", 22 | "pandoc-secnos==2.2.2", 23 | "pandoc-tablenos==2.3.0", 24 | "pandoc-xnos==2.5.0", 25 | "pandocfilters==1.5.1", 26 | "pypandoc-binary==1.13", 27 | "pypandoc==1.13", 28 | "requests==2.32.2", 29 | "lxml==5.1.0", 30 | "watchdog==2.1.9", 31 | "cachelib==0.12.0", 32 | ] 33 | 34 | [project.optional-dependencies] 35 | dev = [ 36 | "pytest", 37 | ] 38 | 39 | [project.scripts] 40 | wikmd = "wikmd.wiki:run_wiki" 41 | 42 | [build-system] 43 | requires = ["setuptools>=61.0"] 44 | build-backend = "setuptools.build_meta" 45 | 46 | [tool.setuptools.packages.find] 47 | where = ["src"] 48 | 49 | [tool.setuptools.package-data] 50 | wikmd = [ 51 | "wikmd-config.yaml", 52 | "plugins/draw/default_draw", 53 | "static/**/*", 54 | "templates/**/*", 55 | "wiki_template/**/*", 56 | ] 57 | 58 | [tool.pytest.ini_options] 59 | pythonpath = [ 60 | "src" 61 | ] 62 | 63 | [tool.ruff] 64 | src = ["", "tests"] 65 | select = ["ALL"] 66 | 67 | [tool.ruff.lint.per-file-ignores] 68 | "tests/*" = [ 69 | # S101: Check for assert 70 | "S101", 71 | 72 | # ANN001: Missing type annotation for public function 73 | "ANN001", 74 | 75 | # ANN201: Missing return type annotation for public function 76 | "ANN201", 77 | 78 | # D100: Missing docstring in public module 79 | "D100", 80 | 81 | # D103: Missing docstring in public function 82 | "D103", 83 | 84 | # PLR2004: Magic numbers 85 | "PLR2004", 86 | ] 87 | -------------------------------------------------------------------------------- /src/wikmd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/__init__.py -------------------------------------------------------------------------------- /src/wikmd/cache.py: -------------------------------------------------------------------------------- 1 | import os 2 | from shutil import rmtree 3 | from typing import Optional 4 | 5 | import cachelib 6 | 7 | 8 | class Cache: 9 | cache: cachelib.FileSystemCache 10 | 11 | def __init__(self, path: str): 12 | if os.path.exists(path): 13 | rmtree(path) # delete an existing cache on start/restart 14 | os.makedirs(path) 15 | self.cache = cachelib.FileSystemCache(path) 16 | 17 | def get(self, key: str) -> Optional[str]: 18 | if self.cache.has(key): 19 | if os.path.getmtime(key) > os.path.getmtime(self.cache._get_filename(key)): 20 | self.cache.delete(key) 21 | return None 22 | return self.cache.get(key) 23 | return None 24 | 25 | def set(self, key: str, content: str): 26 | self.cache.set(key, content, timeout=0) 27 | -------------------------------------------------------------------------------- /src/wikmd/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import yaml 4 | 5 | WIKMD_CONFIG_FILE = "wikmd-config.yaml" 6 | 7 | # Default config parameters 8 | WIKMD_HOST_DEFAULT = "0.0.0.0" 9 | WIKMD_PORT_DEFAULT = 5000 10 | WIKMD_LOGGING_DEFAULT = 1 11 | WIKMD_LOGGING_FILE_DEFAULT = "wikmd.log" 12 | 13 | GIT_EMAIL_DEFAULT = "wikmd@no-mail.com" 14 | GIT_USER_DEFAULT = "wikmd" 15 | 16 | MAIN_BRANCH_NAME_DEFAULT = "main" 17 | SYNC_WITH_REMOTE_DEFAULT = 0 18 | REMOTE_URL_DEFAULT = "" 19 | 20 | WIKI_DIRECTORY_DEFAULT = "wiki" 21 | WIKI_TITLE_DEFAULT = "Wiki" 22 | HOMEPAGE_DEFAULT = "homepage.md" 23 | HOMEPAGE_TITLE_DEFAULT = "homepage" 24 | IMAGES_ROUTE_DEFAULT = "img" 25 | DRAWINGS_ROUTE_DEFAULT = ".drawings" 26 | 27 | HIDE_FOLDER_IN_WIKI = [] 28 | 29 | PLUGINS = [] 30 | PLANTUML_SERVER_URL = "" 31 | 32 | PROTECT_EDIT_BY_PASSWORD = 0 33 | PASSWORD_IN_SHA_256 = "0E9C700FAB2D5B03B0581D080E74A2D7428758FC82BD423824C6C11D6A7F155E" #pw: wikmd 34 | 35 | # if False: Uses external CDNs to serve some files 36 | LOCAL_MODE = False 37 | 38 | IMAGE_ALLOWED_MIME_DEFAULT = ["image/gif", "image/jpeg", "image/png", "image/svg+xml", "image/webp"] 39 | # you need to have cwebp installed for optimization to work 40 | OPTIMIZE_IMAGES_DEFAULT = "no" 41 | 42 | CACHE_DIR = "/dev/shm/wikmd/cache" 43 | SEARCH_DIR = "/dev/shm/wikmd/searchindex" 44 | 45 | SECRET_KEY = '\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O<!\xd5\xa2\xa0\x9fR"\xa1\xa8' 46 | 47 | def config_list(yaml_config, config_item_name, default_value): 48 | """ 49 | Function that gets a config item of type list. 50 | Priority is in the following order either from environment variables or yaml file or default value. 51 | """ 52 | if os.getenv(config_item_name.upper()): 53 | # Env Var in the form `EXAMPLE="a, b, c, d"` or `EXAMPLE="a,b,c,d"` 54 | return [ext.strip() for ext in os.getenv(config_item_name.upper()).split(",")] 55 | elif yaml_config[config_item_name.lower()]: 56 | return yaml_config[config_item_name.lower()] 57 | else: 58 | return default_value 59 | 60 | 61 | class WikmdConfig: 62 | """ 63 | Class that stores the configuration of wikmd. 64 | """ 65 | def __init__(self): 66 | """ 67 | Function that gets the configuration parameters from .yaml file, os environment variables or default values. 68 | Each configuration parameter is stored into a class attribute. 69 | Env. vars take precedence. 70 | """ 71 | __location__ = os.path.realpath( 72 | os.path.join(os.getcwd(), os.path.dirname(__file__))) 73 | 74 | # .yaml config file 75 | with open(os.path.join(__location__, WIKMD_CONFIG_FILE)) as f: 76 | yaml_config = yaml.safe_load(f) 77 | 78 | # Load config parameters from env. vars, yaml or default values (the firsts take precedence) 79 | self.wikmd_host = os.getenv("WIKMD_HOST") or yaml_config.get("wikmd_host", WIKMD_HOST_DEFAULT) 80 | self.wikmd_port = os.getenv("WIKMD_PORT") or yaml_config.get("wikmd_port", WIKMD_PORT_DEFAULT) 81 | self.wikmd_logging = os.getenv("WIKMD_LOGGING") or yaml_config.get("wikmd_logging", WIKMD_LOGGING_DEFAULT) 82 | self.wikmd_logging_file = os.getenv("WIKMD_LOGGING_FILE") or yaml_config.get("wikmd_logging_file", WIKMD_LOGGING_FILE_DEFAULT) 83 | 84 | self.git_user = os.getenv("GIT_USER") or yaml_config.get("git_user", GIT_USER_DEFAULT) 85 | self.git_email = os.getenv("GIT_EMAIL") or yaml_config.get("git_emai", GIT_EMAIL_DEFAULT) 86 | 87 | self.main_branch_name = os.getenv("MAIN_BRANCH_NAME") or yaml_config.get("main_branch_name", MAIN_BRANCH_NAME_DEFAULT) 88 | self.sync_with_remote = os.getenv("SYNC_WITH_REMOTE") or yaml_config.get("sync_with_remote", SYNC_WITH_REMOTE_DEFAULT) 89 | self.remote_url = os.getenv("REMOTE_URL") or yaml_config.get("remote_url", REMOTE_URL_DEFAULT) 90 | 91 | self.wiki_directory = os.getenv("WIKI_DIRECTORY") or yaml_config.get("wiki_directory", WIKI_DIRECTORY_DEFAULT) 92 | self.wiki_title = os.getenv("WIKI_TITLE") or yaml_config.get("wiki_title", WIKI_TITLE_DEFAULT) 93 | self.homepage = os.getenv("HOMEPAGE") or yaml_config.get("homepage", HOMEPAGE_DEFAULT) 94 | self.homepage_title = os.getenv("HOMEPAGE_TITLE") or yaml_config.get("homepage_title", HOMEPAGE_TITLE_DEFAULT) 95 | self.images_route = os.getenv("IMAGES_ROUTE") or yaml_config.get("images_route", IMAGES_ROUTE_DEFAULT) 96 | self.drawings_route = DRAWINGS_ROUTE_DEFAULT 97 | 98 | self.hide_folder_in_wiki = os.getenv("HIDE_FOLDER_IN_WIKI") or yaml_config.get("hide_folder_in_wiki", HIDE_FOLDER_IN_WIKI) 99 | 100 | self.plugins = os.getenv("WIKI_PLUGINS") or yaml_config.get("plugins", PLUGINS) 101 | self.plantuml_server_url = os.getenv("PLANTUML_SERVER_URL") or yaml_config.get("plantuml_server_url", PLANTUML_SERVER_URL) 102 | 103 | self.protect_edit_by_password = os.getenv("PROTECT_EDIT_BY_PASSWORD") or yaml_config.get("protect_edit_by_password", PROTECT_EDIT_BY_PASSWORD) 104 | self.password_in_sha_256 = os.getenv("PASSWORD_IN_SHA_256") or yaml_config.get("password_in_sha_256", PASSWORD_IN_SHA_256) 105 | 106 | self.local_mode = (os.getenv("LOCAL_MODE") in ["True", "true", "Yes", "yes"]) or yaml_config.get("local_mode", LOCAL_MODE) 107 | 108 | self.image_allowed_mime = config_list(yaml_config, "IMAGE_ALLOWED_MIME", IMAGE_ALLOWED_MIME_DEFAULT) 109 | self.optimize_images = os.getenv("OPTIMIZE_IMAGES") or yaml_config.get("optimize_images", OPTIMIZE_IMAGES_DEFAULT) 110 | 111 | self.cache_dir = os.getenv("CACHE_DIR") or yaml_config.get("cache_dir", CACHE_DIR) 112 | self.search_dir = os.getenv("SEARCH_DIR") or yaml_config.get("search_dir", SEARCH_DIR) 113 | 114 | self.secret_key = os.getenv("SECRET_KEY") or yaml_config.get("secret_key", SECRET_KEY) 115 | -------------------------------------------------------------------------------- /src/wikmd/git_manager.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | from typing import Optional 4 | 5 | from flask import Flask 6 | from git import GitCommandError, InvalidGitRepositoryError, NoSuchPathError, Repo 7 | from wikmd.config import WikmdConfig 8 | from wikmd.utils import move_all_files 9 | 10 | TEMP_DIR = "temp" 11 | 12 | 13 | cfg = WikmdConfig() 14 | 15 | 16 | def is_git_repo(path: str) -> bool: 17 | """ 18 | Function that determines if the given path is a git repo. 19 | :return: True if is a repo, False otherwise. 20 | """ 21 | try: 22 | _ = Repo(path).git_dir 23 | return True 24 | except (InvalidGitRepositoryError, NoSuchPathError): 25 | return False 26 | 27 | 28 | class WikiRepoManager: 29 | """ 30 | Class that manages the git repo of the wiki. 31 | The repo could be local or remote (it will be cloned) depending on the config settings. 32 | """ 33 | def __init__(self, flask_app: Flask): 34 | self.flask_app: Flask = flask_app 35 | 36 | self.wiki_directory = cfg.wiki_directory 37 | self.sync_with_remote = cfg.sync_with_remote 38 | self.remote_url = cfg.remote_url 39 | 40 | self.repo: Optional[Repo] = None 41 | 42 | def initialize(self): 43 | if not os.path.exists(self.wiki_directory): 44 | self.flask_app.logger.warning("wiki directory doesn't exist") 45 | return 46 | self.__git_repo_init() 47 | 48 | def __git_repo_init(self): 49 | """ 50 | Function that initializes the git repo of the wiki. 51 | """ 52 | if is_git_repo(self.wiki_directory): 53 | self.__init_existing_repo() 54 | else: 55 | if self.remote_url: # if a remote url has been set, clone the repo 56 | self.__init_remote_repo() 57 | else: 58 | self.__init_new_local_repo() 59 | 60 | # Configure git username and email 61 | if self.repo: # if the repo has been initialized 62 | self.repo.config_writer().set_value("user", "name", cfg.git_user).release() 63 | self.repo.config_writer().set_value("user", "email", cfg.git_email).release() 64 | 65 | def __init_existing_repo(self): 66 | """ 67 | Function that inits the existing repo in the wiki_directory. 68 | Could be local or remote. 69 | """ 70 | self.flask_app.logger.info(f"Initializing existing repo >>> {self.wiki_directory} ...") 71 | try: 72 | self.repo = Repo(self.wiki_directory) 73 | if not self.repo.branches: # if the repo hasn't any branch yet 74 | self.__git_create_main_branch() 75 | self.repo.git.checkout() 76 | except (InvalidGitRepositoryError, GitCommandError, NoSuchPathError) as e: 77 | self.flask_app.logger.error(f"Existing repo initialization failed >>> {str(e)}") 78 | 79 | def __init_remote_repo(self): 80 | """ 81 | Function that inits a remote git repo. 82 | The repo is cloned from the remote_url into the wiki_directory. 83 | Eventually, a 'main' branch is created if missing. 84 | """ 85 | self.flask_app.logger.info(f"Cloning >>> {self.remote_url} ...") 86 | 87 | moved = False 88 | # if the wiki directory is not empty, move all the files into a 'temp' directory 89 | if os.listdir(self.wiki_directory): 90 | self.flask_app.logger.info(f"'{self.wiki_directory}' not empty, temporary moving them to 'temp' ...") 91 | move_all_files(self.wiki_directory, TEMP_DIR) 92 | moved = True 93 | 94 | try: 95 | self.repo = Repo.clone_from(url=self.remote_url, to_path=self.wiki_directory) # git clone 96 | except (InvalidGitRepositoryError, GitCommandError, NoSuchPathError) as e: 97 | self.flask_app.logger.error(f"Cloning from remote repo failed >>> {str(e)}") 98 | 99 | if not self.repo.remotes: # if the remote repo hasn't any branch yet 100 | self.__git_create_main_branch() 101 | 102 | if moved: # move back the files from the 'temp' directory 103 | move_all_files(TEMP_DIR, self.wiki_directory) 104 | os.rmdir(TEMP_DIR) 105 | self.flask_app.logger.info(f"Cloned repo >>> {self.remote_url}") 106 | 107 | def __init_new_local_repo(self): 108 | """ 109 | Function that inits a new local git repo into the wiki_directory. 110 | It creates also the 'main' branch for the repo. 111 | """ 112 | self.flask_app.logger.info(f"Creating a new local repo >>> {self.wiki_directory} ...") 113 | try: 114 | self.repo = Repo.init(path=self.wiki_directory) 115 | self.__git_create_main_branch() 116 | self.repo.git.checkout() 117 | except (InvalidGitRepositoryError, GitCommandError, NoSuchPathError) as e: 118 | self.flask_app.logger.error(f"New local repo initialization failed >>> {str(e)}") 119 | 120 | def __git_create_main_branch(self): 121 | """ 122 | Function that creates the 'main' branch for the wiki repo. 123 | The repo could be local or remote; in the latter case, local changes are pushed. 124 | """ 125 | self.flask_app.logger.info(f"Creating 'main' branch ...") 126 | self.repo.git.checkout("-b", cfg.main_branch_name) 127 | self.__git_commit("First init commit") 128 | if self.sync_with_remote: 129 | self.__git_push() 130 | 131 | def __git_pull(self): 132 | """ 133 | Function that pulls from the remote wiki repo. 134 | """ 135 | self.flask_app.logger.info(f"Pulling from the repo >>> {self.wiki_directory} ...") 136 | try: 137 | self.repo.git.pull() # git pull 138 | except Exception as e: 139 | self.flask_app.logger.info(f"git pull failed >>> {str(e)}") 140 | 141 | def __git_commit(self, message: str): 142 | """ 143 | Function that makes a generic commit to the wiki repo. 144 | :param message: commit message. 145 | """ 146 | try: 147 | self.repo.git.add("--all") # git add --all 148 | self.repo.git.commit('-m', message) # git commit -m 149 | self.flask_app.logger.info(f"New git commit >>> {message}") 150 | except Exception as e: 151 | self.flask_app.logger.error(f"git commit failed >>> {str(e)}") 152 | 153 | def __git_commit_page_changes(self, page_name: str = "", commit_type: str = ""): 154 | """ 155 | Function that commits page changes to the wiki repo. 156 | :param commit_type: could be 'Add', 'Edit' or 'Remove'. 157 | :param page_name: name of the page that has been changed. 158 | """ 159 | date = datetime.datetime.now() 160 | message = f"{commit_type} page '{page_name}' on {str(date)}" 161 | self.__git_commit(message=message) 162 | 163 | def __git_push(self): 164 | """ 165 | Function that pushes changes to the remote wiki repo. 166 | It sets the upstream (param -u) to the active branch. 167 | """ 168 | try: 169 | self.repo.git.push("-u", "origin", self.repo.active_branch) # git push -u origin main|master 170 | self.flask_app.logger.info("Pushed to the repo.") 171 | except Exception as e: 172 | self.flask_app.logger.error(f"git push failed >>> {str(e)}") 173 | 174 | def git_sync(self, page_name: str = "", commit_type: str = ""): 175 | """ 176 | Function that manages the synchronization with a git repo, that could be local or remote. 177 | If SYNC_WITH_REMOTE is set, it also pulls before committing and then pushes changes to the remote repo. 178 | :param commit_type: could be 'Add', 'Edit' or 'Remove'. 179 | :param page_name: name of the page that has been changed. 180 | """ 181 | if self.sync_with_remote: 182 | self.__git_pull() 183 | 184 | self.__git_commit_page_changes(page_name=page_name, commit_type=commit_type) 185 | 186 | if self.sync_with_remote: 187 | self.__git_push() 188 | 189 | def git_pull(self): 190 | """ 191 | Function that manages the synchronization with a git repo, that could be local or remote. 192 | If SYNC_WITH_REMOTE is set, it also pulls before committing and then pushes changes to the remote repo. 193 | :param commit_type: could be 'Add', 'Edit' or 'Remove'. 194 | :param page_name: name of the page that has been changed. 195 | """ 196 | if self.sync_with_remote: 197 | self.__git_pull() 198 | -------------------------------------------------------------------------------- /src/wikmd/image_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import tempfile 5 | from base64 import b32encode 6 | from hashlib import sha1 7 | 8 | from werkzeug.utils import safe_join, secure_filename 9 | 10 | 11 | class ImageManager: 12 | """ 13 | Class that manages the images of the wiki. 14 | It can save, optimize and delete images. 15 | """ 16 | 17 | def __init__(self, app, cfg): 18 | self.logger = app.logger 19 | self.cfg = cfg 20 | self.images_path = os.path.join(self.cfg.wiki_directory, self.cfg.images_route) 21 | self.temp_dir = "/tmp/wikmd/images" 22 | # Execute the needed programs to check if they are available. Exit code 0 means the programs were executed successfully 23 | self.logger.info("Checking if webp is available for image optimization ...") 24 | self.can_optimize = os.system("cwebp -version") == 0 and os.system("gif2webp -version") == 0 25 | if not self.can_optimize and self.cfg.optimize_images in ["lossless", "lossy"]: 26 | self.logger.error("To use image optimization webp and gif2webp need to be installed and in the $PATH. They could not be found.") 27 | 28 | def save_images(self, file): 29 | """ 30 | Saves the image from the filepond upload. 31 | The image is renamed to the hash of the content, so the image is immutable. 32 | This makes it possible to cache it indefinitely on the client side. 33 | """ 34 | img_file = file["filepond"] 35 | original_file_name, img_extension = os.path.splitext(img_file.filename) 36 | 37 | temp_file_handle, temp_file_path = tempfile.mkstemp() 38 | img_file.save(temp_file_path) 39 | 40 | if self.cfg.optimize_images in ["lossless", "lossy"] and self.can_optimize: 41 | temp_file_handle, temp_file_path, img_extension = self.__optimize_image(temp_file_path, img_file.content_type) 42 | 43 | # Does not matter if sha1 is secure or not. If someone has the right to edit they can already delete all pages. 44 | hasher = sha1() 45 | with open(temp_file_handle, "rb") as f: 46 | data = f.read() 47 | hasher.update(data) 48 | 49 | # Using base32 instead of urlsafe base64, because the Windows file system is case-insensitive 50 | img_digest = b32encode(hasher.digest()).decode("utf-8").lower()[:-4] 51 | hash_file_name = secure_filename(f"{original_file_name}-{img_digest}{img_extension}") 52 | hash_file_path = os.path.join(self.images_path, hash_file_name) 53 | 54 | # We can skip writing the file if it already exists. It is the same file, because it has the same hash 55 | if os.path.exists(hash_file_path): 56 | self.logger.info(f"Image already exists '{img_file.filename}' as '{hash_file_name}'") 57 | else: 58 | self.logger.info(f"Saving image >>> '{img_file.filename}' as '{hash_file_name}' ...") 59 | shutil.move(temp_file_path, hash_file_path) 60 | 61 | return hash_file_name 62 | 63 | def cleanup_images(self): 64 | """Deletes images not used by any page""" 65 | saved_images = set(os.listdir(self.images_path)) 66 | # Don't delete .gitignore 67 | saved_images.discard(".gitkeep") 68 | 69 | # Matches [*](/img/*) it does not matter if images_route is "/img" or "img" 70 | image_link_pattern = fr"\[(.*?)\]\(({os.path.join('/', self.cfg.images_route)}.+?)\)" 71 | image_link_regex = re.compile(image_link_pattern) 72 | used_images = set() 73 | # Searching for Markdown files 74 | for root, sub_dir, files in os.walk(self.cfg.wiki_directory): 75 | if os.path.join(self.cfg.wiki_directory, '.git') in root: 76 | # We don't want to search there 77 | continue 78 | if self.images_path in root: 79 | # Nothing interesting there too 80 | continue 81 | for filename in files: 82 | path = os.path.join(root, filename) 83 | try: 84 | with open(path, "r", encoding="utf-8", errors="ignore") as f: 85 | content = f.read() 86 | matches = image_link_regex.findall(content) 87 | for _caption, image_path in matches: 88 | used_images.add(os.path.basename(image_path)) 89 | except: 90 | self.logger.info(f"ignoring {path}") 91 | 92 | not_used_images = saved_images.difference(used_images) 93 | for not_used_image in not_used_images: 94 | self.delete_image(not_used_image) 95 | 96 | def delete_image(self, image_name): 97 | image_path = safe_join(self.images_path, image_name) 98 | self.logger.info(f"Deleting file >>> {image_path}") 99 | try: 100 | os.remove(image_path) 101 | except IsADirectoryError | FileNotFoundError: 102 | self.logger.error(f"Could not delete '{image_path}'") 103 | 104 | def __optimize_image(self, temp_file_path_original, content_type): 105 | """ 106 | Optimizes gif, jpg and png by converting them to webp. 107 | gif and png files are always converted lossless. 108 | jpg files are either converted lossy or near lossless depending on cfg.optimize_images. 109 | 110 | Uses the external binaries cwebp and gif2webp. 111 | """ 112 | 113 | temp_file_handle, temp_file_path = tempfile.mkstemp() 114 | if content_type in ["image/gif", "image/png"]: 115 | self.logger.info(f"Compressing image lossless ...") 116 | if content_type == "image/gif": 117 | os.system(f"gif2webp -quiet -m 6 {temp_file_path_original} -o {temp_file_path}") 118 | else: 119 | os.system(f"cwebp -quiet -lossless -z 9 {temp_file_path_original} -o {temp_file_path}") 120 | os.remove(temp_file_path_original) 121 | 122 | elif content_type in ["image/jpeg"]: 123 | if self.cfg.optimize_images == "lossless": 124 | self.logger.info(f"Compressing image near lossless ...") 125 | os.system(f"cwebp -quiet -near_lossless -m 6 {temp_file_path_original} -o {temp_file_path}") 126 | elif self.cfg.optimize_images == "lossy": 127 | self.logger.info(f"Compressing image lossy ...") 128 | os.system(f"cwebp -quiet -m 6 {temp_file_path_original} -o {temp_file_path}") 129 | os.remove(temp_file_path_original) 130 | 131 | return temp_file_handle, temp_file_path, ".webp" 132 | -------------------------------------------------------------------------------- /src/wikmd/knowledge_graph.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from urllib.parse import unquote 4 | 5 | from wikmd.config import WikmdConfig 6 | 7 | cfg = WikmdConfig() 8 | 9 | 10 | def extend_ids(links): 11 | for link in links: 12 | for l in link["links"]: 13 | for i in links: 14 | if i["path"] == l["filename"]: 15 | l["id"] = i["id"] 16 | return links 17 | 18 | 19 | def find_links(): 20 | links = [] 21 | # regex for links (excluding images) 22 | pattern = r'[^!]\[(.+?)\]\((.+?)\)' 23 | id = 1 24 | # walk through all files 25 | for root, subfolder, files in os.walk(cfg.wiki_directory): 26 | for item in files: 27 | pagename, _ = os.path.splitext(item) 28 | path = os.path.join(root, item) 29 | value = { 30 | "id": id, 31 | "pagename": pagename, 32 | "path": path[len(cfg.wiki_directory)+1:-len(".md")], 33 | "weight": 0, 34 | "links": [], 35 | } 36 | id += 1 37 | if os.path.join(cfg.wiki_directory, '.git') in str(path): 38 | # We don't want to search there 39 | continue 40 | if os.path.join(cfg.wiki_directory, cfg.images_route) in str(path): 41 | # Nothing interesting there too 42 | continue 43 | if os.path.join(cfg.wiki_directory, '.drawings') in str(path): 44 | # Nothing interesting there too 45 | continue 46 | with open(os.path.join(root, item), encoding="utf8", errors='ignore') as f: 47 | fin = f.read() 48 | print("--------------------") 49 | print("filename: ", pagename) 50 | try: 51 | for match in re.finditer(pattern, fin): 52 | description, url = match.groups() 53 | # only show files that are in the wiki. Not external sites. 54 | if url.startswith("/"): 55 | url = url[1:] 56 | url = unquote(url) 57 | if os.path.exists(os.path.join(cfg.wiki_directory, f"{url}.md")): 58 | info = { 59 | "filename": url, 60 | } 61 | value["links"].append(info) 62 | print(url) 63 | 64 | except Exception as e: 65 | print("error: ", e) 66 | 67 | links.append(value) 68 | links = extend_ids(links) 69 | return links 70 | -------------------------------------------------------------------------------- /src/wikmd/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/plugins/__init__.py -------------------------------------------------------------------------------- /src/wikmd/plugins/alerts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/plugins/alerts/__init__.py -------------------------------------------------------------------------------- /src/wikmd/plugins/alerts/alerts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from flask import Flask 5 | from wikmd.config import WikmdConfig 6 | 7 | 8 | class Plugin: 9 | def __init__(self, flask_app: Flask, config: WikmdConfig, web_dep ): 10 | self.name = "Alerts system" 11 | self.plugname = "alerts" 12 | self.flask_app = flask_app 13 | self.config = config 14 | self.this_location = os.path.dirname(__file__) 15 | self.web_dep = web_dep 16 | 17 | def get_plugin_name(self) -> str: 18 | """ 19 | returns the name of the plugin 20 | """ 21 | return self.name 22 | 23 | def process_before_cache_html(self, html: str) -> str: 24 | """ 25 | returns the html file after process the input file 26 | """ 27 | return self.search_in_html_for_informational(html) 28 | 29 | 30 | def search_in_html_for_informational(self,file: str) -> str: 31 | """ 32 | search for [[informational]](text to show) in "file" and replace it with the content of a corresponding drawfile 33 | """ 34 | 35 | warning_icon = "<i class=\"bi-exclamation-triangle-fill mr-2\"></i>" 36 | info_icon = "<i class=\"bi-info-circle-fill mr-2\"></i>" 37 | danger_icon = "<i class=\"bi-exclamation-octagon-fill mr-2\"></i>" 38 | success_icon = "<i class=\"bi-check-circle-fill mr-2\"></i>" 39 | 40 | 41 | result = file 42 | result = re.sub(r"(?si)(\<p)()(\>)\[\[warning\]\](.*?)\<\/p\>", r"<div class='alert alert-warning d-flex'\3"+warning_icon+r" <a>\4</a></div>", result) 43 | result = re.sub(r"(?si)(\<p)()(\>)\[\[info\]\](.*?)\<\/p\>", r"<div class='alert alert-info d-flex'\3"+info_icon+r" <a>\4</a></div>", result) 44 | result = re.sub(r"(?si)(\<p)()(\>)\[\[danger\]\](.*?)\<\/p\>", r"<div class='alert alert-danger d-flex'\3"+danger_icon+r" <a>\4</a></div>", result) 45 | result = re.sub(r"(?si)(\<p)()(\>)\[\[success\]\](.*?)\<\/p\>", r"<div class='alert alert-success d-flex'\3"+success_icon+r" <a>\4</a></div>", result) 46 | 47 | return result 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/wikmd/plugins/draw/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/plugins/draw/__init__.py -------------------------------------------------------------------------------- /src/wikmd/plugins/draw/default_draw: -------------------------------------------------------------------------------- 1 | <img title="Click to edit image" onclick="DiagramEditor.editElement(this);" id="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHsAAABACAYAAAA6eggRAAAAAXNSR0IArs4c6QAAA1l0RVh0bXhmaWxlACUzQ214ZmlsZSUyMGhvc3QlM0QlMjJlbWJlZC5kaWFncmFtcy5uZXQlMjIlMjBtb2RpZmllZCUzRCUyMjIwMjMtMDEtMTBUMjMlM0EwOCUzQTE1LjYwOVolMjIlMjBhZ2VudCUzRCUyMjUuMCUyMChYMTElM0IlMjBMaW51eCUyMHg4Nl82NCklMjBBcHBsZVdlYktpdCUyRjUzNy4zNiUyMChLSFRNTCUyQyUyMGxpa2UlMjBHZWNrbyklMjBDaHJvbWUlMkYxMDguMC4wLjAlMjBTYWZhcmklMkY1MzcuMzYlMjIlMjBldGFnJTNEJTIyYnZnWWFEa2ZIcjdpdVRJYVRtR2slMjIlMjB2ZXJzaW9uJTNEJTIyMjAuOC4zJTIyJTIwdHlwZSUzRCUyMmVtYmVkJTIyJTNFJTNDZGlhZ3JhbSUyMGlkJTNEJTIyZW1ZUlNERFJrRFAyWWQzRVp5M3IlMjIlMjBuYW1lJTNEJTIyUGFnZS0xJTIyJTNFalpQTmJvUWdFSUNmeHZzcVcyT3Z0ZHZ0cFNjUFBWT1pDbGtVZzdocW43NGd3eW94VFhyUm1XOWclMkZrbEkyYzVYVFh2JTJCb1JqSUpEdXhPU0d2U1phbFJWcllueU9MSndWNThxRFJnbmwwMmtBbGZnQnZCam9LQmdNeWo0eFMwb2clMkJoclhxT3FoTnhLaldhb3FQZlNzWlIlMkIxcEE5RUpCNnFheWlQOUZNeHdyQ0xMTiUyRjRPb3VFaGNwbyUyRmU4c1hyVyUyQk5WbU9IOFRyVmdiZTBOTGpCSEFaT21acDJpRndTVW1xbGpKZmF1UVRwMmhwMzdPMFBLNlk4bUNVVUVYTFYwSmwlMkZlUWhGM0trYzBVY3BSWDJ6aUlPR2RRajJBMHdZSjNOSG1LQ05wdTBoJTJCTVNGZ2FxbnRkTW51eVVKZWVHbWxWWkxyZmdvM2lscnY0Q2hka3diUzd1RE5qRHZFSlp4QmRXQzBZczlnbFp5UHZzclM3eFcwemJMTkl5QjclMkJhWUk2TzRQczNEODlZMksyRG5ncnFOYkxYdG5nUzUlMkZBSSUzRCUzQyUyRmRpYWdyYW0lM0UlM0MlMkZteGZpbGUlM0WwqEjDAAAPNklEQVR4Xu1dCXBURRPuXeMSYgIJ4ReCAfFAIT8pFAWPEgUpCQWxEki8C4L8KES0kuIQvC9ADg0JqKE8IAEtf0tAkCiHBwIqVIEHSbhBOfQPUXKRGMISN399nX3J7PF25719L9mAUwWE3Zme7v66e3qmJ+9ZyGuzXGu1WsZERUWlVFdX97Tb7aFKNwsRNXgZo/Y5d/X5pXcO1D/VTkz7CK08+ehv1OQiHeFnm81WFxERcbSiomKVw+FYQUQHPLhx9sc/Li08PDy3vr5+fHp6uiUpKenifv36UWRkpIHSC6RgNZZG4/FgxJwZg46qLtmFQZWVlbR7925au3btudzc3IaQkJClNTU16d4EFXXcLTo6+qvEkYk9snOyw9QAFpnTxWjQqdvT+HSx6KGMQLTjZawEOQCfmZlZW1BQcLysrGwokeV/YhxuAjs6OnpfZmZm72effVaXrOfrIAkdB53os2bNouzs7P1lZWV9ROYYbITulJSUsXl5eWGenLdFcQPRfxuQV4LFtLS0M6tXr84XQzrAvjY0NLSwpKTEpn9tlpg9EP07k0KLzDQyfWR4kaEj00dmLhP6IKTHxMScq6ur609ExZwnW63WWRkZGU9mZWVdHPCcBgtvMLmAxdNPQIMkGrr642fKlCn1OTk5KxwOx3gGG2v1qlWret9xxx3+xv7zfVvSQAPRlq1baPTo0cfKy8vvIqJDFpvNdqa0tDQ0MrKjsAGSMa82nJfLiBcIsNL0XTtKD/PJWzMVhPIuXbrY7XY7wN7F2+2GBm/HJP6kNYY1f7O09vdtXUqLhXNwhO0TmsFm4Y3SgFF0WtsiTJ4/EDU5wR5MRLWawfYuVyDsqGnKDJomo2I0eTUVaFCNALZDBWwva4mGCYyRWeuEWvsbw6UvKi3OkZcJBbAbSxT61mzzlRXwDCZq20TSAYstEggCsA3OQs3WvB/6Zk8fCPqtC7aHZoxSlVY6WvsHonKdYzWwqNaVwbbQYJQWWzSMe2dIg0Q6dXa+DpPRnIpnywxtIbVJsyLdUUPRXAPNFlKHOA1z52RRhlPpMP7555/Tyy+/zMXxsLAwwpHq/Pnz6eqrr6aff/6ZUlNT6fDhw/Taa6/R/v376d1331UVH0WW4uJiio2NVe3zxhtv0I4dO+j9999vETXm5eXRuHHjTJlLTT9ffvkl9e7d26semvlpoM6d/8W6gK4Daaqe3UCWphsjBQUF9OCDD9LChQspOTmZ6uvrKSsri8AQgD127FgT2LW1tfx9hw4dmC9vFtcSYP/999900UUXSekGO5CYmBg6efKkVH/ZTorsItiifpKSkuipp56im2++2YWkOz+dO3c2E2xXca677joaM2YMTZ061eWL9957jxITE6mkpIRSUlPpiJtn79mzhyZMmEAnTpygK6+8kvLz8+mKK67gq01FxcXUPTaWnnnmGSosLMRVGrJarU304dnfffcdAbRvv/2WunbtSp988gldfvnldPDgQXr00UcZnPDwcFq0aBHdeuut9OOPP9L48eOpb9++9Pvvv9PmzZvps88+Y4Xa7XbmATwDWLGNGjWK54+Li6P169cTzpEnTZpEf/75J7ULDaU5s2fT3Xff7YGxGh/o+Oqrr1Jubi516tSJ7rnnHlq2bJlL5OvVqxe98MILdNlll9GCBQtQpGii787P9ddfz1H1rbfeYl0Dh6effpr7y8inEPYbxquqqhico0ePsqK9NW9h6p133iHcWXvppZcIzM+fP4++/nozbdiwgekhjAPMefPm0datWxk0sQFsKANA9+nThyMLDGX27NnUv39/Sk9Pp0ceeYR27dxJScnJ9Ouvv9KBAwfolltuYUDvu+8+NgaM3bZtGxvA66+/znOuXr3aZa5Tp05xKK2rqyOHw8F9n3vuOXrggQcIBgtDOnLkMIfTxtbosyIfO3fu5KgHPo4cOcJj9u3bh+IDOwrCsPsyh3mw3CmerUQCkR/MBs++//77afHixWzo8fHxbJCnT5+Wks8FbF/Z+PHjxxnkM2fOUGhoqNew7A1sKAtggym0c+fO0dmzZxlUgJ2/fDlNmzqVvvnmG7Zu9wawN23aRJ9++ikrd9GixfTDDz/QK6+8wh4IQZVIMGDAAM4VoqOjaeDAgVRTU8PfLV++nD788EP2VjR8HhUVxaCKIV5ULoBCJAN9pyewAc2cOZMQdpUGvcT1iaPT1Z58AGTMiWiBtnHjRpo8ebJfsBXa3sCGLmBcaIgW0AWMWEY+ac/+66+/KCI8nA4eOqSaIHgDe+KkiZSaksrruXsD2EjyIiIiOLkLbd/e40ape4Km/D8zM5M9QUzuwCNCJpKdhIQEDuFoMIAXX3yRPUNpp6uqaO++fbwseFPu9u3b2YtEvkeOHMnRCUuS0nbt2qXKB7wPf5bl5bFc8HpECX+e7QtsMUFT1vA1a9Z4yIdIDGMT5ZMGGx3hORAYihMbwuy9997LXuuejcOzEaYwObwMa+Yvv/zCgABsMIoQBm/Mycnx6tliNs5gb99Bc+fNZbpKxBAHYmkYPnw4/fbbb/wxMvmVK1fyXL6aN89W+Ma4m266iXD5Uly3MYcaH7lLcmnjho1N8yLBhZEaDTb0IyOfJrC/+OILDmFz585lq0dDNv7RRx9xcoXQ5w421uz4+L40bdp03tIAUIS2Des3UGRU45oN78b688EHH9Dgwai8NTc1zwaAN9xwA02bNo29BUlURkYGvf3225xXKGBj/fvjj1KK7xvP6zQSInjYihUrOKETG4CF0eFf8AQQkTgiT0DkGTp0KAOFJUBsanxAH0OGDOH1/tJLL+X8AXTcwUbihURueMLw5svyDURVp5v5ueSSSzgyeXr2doqI6MD68yefJrDRGXtCJFs//fQTr91QKvbZ3bp1U91nFxUV04QJ/+GQeM0117hk48o+G2sywCosKqSI8IgmXfoCGyFy4sSJnOUjakyZMoWzZ3fPBjElW8WWB8sGkpzbbrvNw9GHDRvG2TzOE9q3b8/04PH4GdnyXXfhgodrU+MDvZ5//nmCwXfs2JFpZWdnszGK5xDIsJE04rrvE0884UJc5GfEiBFewG7cdzfKN5Nqa8/4lA/EfWfjMscyPgOk+pcypGX66Jxe5QRAPzXTRxqgDL9bL9OFkJzAAFklZ5Lp5oUbQ4s6MtLK9HGVxQVs/KqVQ+sdNO1zymjzAuxjviLbjGcbg77BCjWYXLOM5hAOgqqXOYIZYxyBUJGTS66XFj7UKV5gnu2qNEOXWS14tFJfw8A23kJFjXhSV87Xcf6NvT/2sIY2cwUylFVZYoaBLTuhUf0UsHEsiNM08UhU7xxayqJa59BlO7oGqXMWONhqDMnsTjRoDMWExx9/nAsYDz30EJ/gFRUVcWVL8WzUgXHAsnbtGnI4GvhUDkeyISEhXFTBePyMAxkcCOHkqby83KMsigIKqms4Bu7RowefunXv3p1PAXESdvZsHRUX7+HPUH178803+Swcx8mocGluTl0ZjK0HG/JgG8WJBuNQuIXHQelLly6lhIRhlJu7hIHDyVwz2Ido3boCrk6hIgTBUAFDLRv1ZFTuUPpEoQSgAEyUIysqKlzKogAfVTgcdaL+DcMArSVLlhBO9ebMmUN79+7l06qePXsyPRgUyrSoseMyh7HPCjFK8f5O0DSbqMqAAPmFAlHpUoofKFHiGBPHpe6ejQqYUhsHUAA5JSWFAQWQaBiDCwwYD5piWRTfV1dXM5hoKCHi4gEiA8DesmULffzxx/zdoEGD+JgTxSBU2uLi/k1VVY0l3dZrorJdFd/ynq1DC99//z0XJXC2rDQUCHBZQQQbRZHp06dziQ+CoT8iwJ133kljx451SeLatWvHBRyALZZFcXkBno9lAw2ej6iC2gDARmkT17HQsEygIIPbOuADZ9WomQdrkwe7FSUAePBMxbOhUHieu2fDk3FBAuEaazvqzwi1qEUDGBgDWmlpKdd7Fc8Wy6LwZIRqXAxAEogqG8B1Bxs+M6QlwTYgB5IC2+z9qL8oj0QJ6ygSJwCDyhHWZniu6Nkos8IocEcLt2ABMkIsqnUxXWNozdo1dPvtt3P5Egka1mwYkAg2vBdevW7dOvZqrPcwLpQY9Xu2Pwn1eZJWqlJge7CidRZdsrhOgis+uACAjBsei5spyKZRhlSycYR7hGubzUYDBg6kUcnJ9PDDD7ORYC2fMWMGe+tjkx+jGU/O4CQMgIpgw/txSaG8rJxiu8dyvRl3y3CREet88Ifx8+oETZ+lKb+wCOtGdo81G/fMcFnhQmn6PFtGO/owkaGsq8+NN97IWyP8wZVm7NMR6i+kZh7YQaZFhHjcGMHVI1wVwjUmXAsKuubHSdQ3Vv4lMQVs7/ya7epm0/evzGDvEdBvcQaiXumx3FH7vkMb/eCASZpnJ7ta+5vi2cGhuiDkQis6Rp2ZO+mYDrZW+RgiXYOCAVyjGDeKjqtOTAc7GCAwlAdzcDCUxWZirumcxcK/OMkX9Fv0yQvGS6czT21V8LRPrn1Es6YvYM/WrjbtI4w36UAouoDd/OxStVdDtHVxA1FVYGNbW3PCs0uHcRjX9lRi9e1QawsWGCzBMdpoHaIO73wqcRqD7fG8caNnDA49BpbtB7NOfPDmfN74fx0OBz/sRuVNAjLSyfQJFqTPHz5kU1LhTQJ4sPwJIuJnl/I7QkaPHj0uPz+/6f1dbUM9xhmcNkraereGLtPS0upWrly5qba2Nss5Pz+VmFt0dPThzMzMq8x7+48BCjKAhKj4gMlpJaC1v04rwa8DL1y4EG8Q4LXaeWTFzxtX2lWdOnX6KjExsUtOTk6o/pfD+OGwhQTWqacmzci/VC54BELozsjIqCsoKCgtLy/HY65OCWDzmwTE1issLGyBw+EY4XxjX0hgb+zTqgj1/mrfaJ0hECPwGNuqkzdyU1lRSbsL+Y199Xhjn9VqXS+EbnRxkIVOUgMd8mbAA/D6J6vVmhAZGTmopqYmxm632/QpyagXUeqb/UIYZbPZ7OHh4SWVlZXbHA4HrsciGRNbLZ7ngw9cwHZCA2D7ERGSteYn0klrrq0ArIFPDV2l1WR+RwcR1RERrubYPcBunp+l60VEymMB5Zcw84X4ZwYvGhDsEYsLWgle7yR29QcibuZ1wQP39Hu6f2y0OY623r7e56yVkn9JWqEHC8F/KZ6MpKyULFTb9IxxJ1v/Bw2R64jwecc8AAAAAElFTkSuQmCC" style=""> -------------------------------------------------------------------------------- /src/wikmd/plugins/draw/draw.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import uuid 5 | 6 | from flask import Flask 7 | from wikmd.config import WikmdConfig 8 | 9 | 10 | class Plugin: 11 | def import_head(self): 12 | return "<script type='text/javascript' src='/static/js/drawio.js'></script>" 13 | 14 | def __init__(self, flask_app: Flask, config: WikmdConfig, web_dep): 15 | self.name = "DrawIO integration" 16 | self.plugname = "draw" 17 | self.flask_app = flask_app 18 | self.config = config 19 | self.this_location = os.path.dirname(__file__) 20 | self.web_dep = web_dep 21 | self.drawings_folder = None 22 | 23 | def post_setup(self) -> bool: 24 | """ 25 | configure the drawings folder 26 | returns True if folder did not exist and created it 27 | """ 28 | self.drawings_folder = os.path.join(self.config.wiki_directory, self.config.drawings_route) 29 | if not os.path.exists(self.drawings_folder): 30 | os.mkdir(self.drawings_folder) 31 | 32 | # allow backward compatibility and move all the contents in the plugins 33 | # drawing folder to the wiki drawings folder 34 | if os.path.exists(os.path.join(self.this_location, "drawings")): 35 | for item in os.listdir(os.path.join(self.this_location, "drawings")): 36 | print(f"copy {item} to {self.drawings_folder}") 37 | shutil.copy2(os.path.join(self.this_location, "drawings",item), os.path.join(self.drawings_folder, item)) 38 | return True 39 | 40 | return False 41 | 42 | def get_plugin_name(self) -> str: 43 | """ 44 | returns the name of the plugin 45 | """ 46 | return self.name 47 | 48 | def process_md(self, md: str) -> str: 49 | """ 50 | returns the md file after process the input file 51 | """ 52 | return self.search_for_pattern_and_replace_with_uniqueid(md) 53 | 54 | def process_html(self, html: str) -> str: 55 | """ 56 | returns the html file after process the input file 57 | """ 58 | return self.search_in_html_for_draw(html) 59 | 60 | def communicate_plugin(self, request): 61 | """ 62 | communication from "/plug_com" 63 | """ 64 | id = request.form['id'] 65 | image = request.form['image'] 66 | 67 | self.flask_app.logger.info(f"Plug/{self.name} - changing drawing {id}") 68 | 69 | # look for folder 70 | location = os.path.join(self.drawings_folder, id) 71 | if os.path.exists(location): 72 | file = open(location, "w") 73 | file.write(image) 74 | file.close() 75 | 76 | return "ok" 77 | 78 | def look_for_existing_drawid(self, drawid: str) -> str: 79 | """ 80 | look for a drawId in the wiki/draw folder and return the file as a string 81 | """ 82 | try: 83 | file = open(os.path.join(self.drawings_folder, drawid), "r") 84 | return file.read() 85 | except Exception: 86 | print("Did not find the file") 87 | return "" 88 | 89 | def create_draw_file(self, filename: str) -> None: 90 | """ 91 | Copy the default drawing to a new one with this filename 92 | """ 93 | 94 | if self.drawings_folder == None: 95 | self.flask_app.logger.warning(f"DrawIO folder does not exist yet. Trying to create it now.") 96 | self.post_setup() 97 | 98 | path_to_file = os.path.join(self.drawings_folder, filename) 99 | shutil.copyfile(os.path.join(self.this_location, "default_draw"), path_to_file) 100 | s = open(path_to_file,"r") 101 | result = re.sub("id=\"\"","id=\"" + filename + "\"",s.read()) 102 | s.close() 103 | s = open(path_to_file,"w") 104 | s.write(result) 105 | s.close() 106 | 107 | 108 | def search_for_pattern_and_replace_with_uniqueid(self, file: str) -> str: 109 | """ 110 | search for [[draw]] and replace with draw_<uniqueid> 111 | """ 112 | filename = "draw_" + str(uuid.uuid4()) 113 | result = re.sub(r"^\[\[draw\]\]", "[[" + filename + "]]", file, flags=re.MULTILINE) 114 | print(file) 115 | self.create_draw_file(filename) 116 | return result 117 | 118 | def search_in_html_for_draw(self,file: str) -> str: 119 | """ 120 | search for [[draw_<unique_id>]] in "file" and replace it with the content of a corresponding drawfile 121 | """ 122 | draws = re.findall(r"\[\[(draw_.*)\]\]", file) 123 | result = file 124 | for draw in draws: 125 | result = re.sub(r"\[\["+draw+r"\]\]", self.look_for_existing_drawid(draw), result) 126 | return result 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/wikmd/plugins/draw/drawings/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/plugins/draw/drawings/.gitkeep -------------------------------------------------------------------------------- /src/wikmd/plugins/embed-pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/plugins/embed-pages/__init__.py -------------------------------------------------------------------------------- /src/wikmd/plugins/embed-pages/embed-pages.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from flask import Flask 5 | from wikmd.config import WikmdConfig 6 | 7 | 8 | class Plugin: 9 | def __init__(self, flask_app: Flask, config: WikmdConfig, web_dep): 10 | self.name = "embed-pages" 11 | self.plugname = "embed-pages" 12 | self.flask_app = flask_app 13 | self.config = config 14 | self.this_location = os.path.dirname(__file__) 15 | self.web_dep = web_dep 16 | 17 | def get_plugin_name(self) -> str: 18 | """ 19 | returns the name of the plugin 20 | """ 21 | return self.name 22 | 23 | def process_md(self, md: str) -> str: 24 | """ 25 | returns the md file after process the input file 26 | """ 27 | return md 28 | 29 | def process_html(self, html: str) -> str: 30 | """ 31 | returns the html file after process the input file 32 | """ 33 | return self.search_for_page_implementation(html) 34 | 35 | def request_html(self, get_html_callback): 36 | self.get_html = get_html_callback 37 | 38 | def search_for_page_implementation(self, file: str) -> str: 39 | """ 40 | search for [[ page: test/testpage ]] in "file" and replace it with the content of a linked page 41 | """ 42 | pages = re.findall(r"\<p\>\[\[\s*page:\s*(.*?)\s*\]\]\<\/p\>", file) 43 | result = file 44 | for page in pages: 45 | # get the html of the page 46 | try: 47 | raw_page_html, _ = self.get_html(page) 48 | except: 49 | return file 50 | 51 | integrate_html = f"<div id=\"{page}\" class=\"html-integration\"><p><i>{page}</i></p>{raw_page_html}</div>" 52 | 53 | # integrate the page into this one. 54 | result = re.sub(r"\<p\>\[\[\s*page:\s*"+page+r"\s*\]\]\<\/p\>", integrate_html, result) 55 | 56 | return result 57 | 58 | -------------------------------------------------------------------------------- /src/wikmd/plugins/load_plugins.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from flask import Flask 4 | from wikmd.config import WikmdConfig 5 | 6 | 7 | class PluginLoader(): 8 | """ 9 | The plugin loader will load all plugins inside "plugins" folder 10 | a plugin should have a folder and a file inside the folder, 11 | both with the name of the plugin 12 | """ 13 | def __init__(self, flask_app: Flask, config: WikmdConfig, web_deps= None, plugins:list=[], ): 14 | # Checking if plugin were sent 15 | if plugins != []: 16 | # create a list of plugins 17 | self._plugins = [ 18 | importlib.import_module(f"wikmd.plugins.{plugin}.{plugin}",".").Plugin(flask_app, config, web_deps) for plugin in plugins 19 | ] 20 | else: 21 | self._plugins = [] 22 | 23 | for plugin in self._plugins: 24 | print(plugin.get_plugin_name()) 25 | 26 | def get_plugins(self): 27 | """ 28 | returns a list of plugins 29 | """ 30 | return self._plugins -------------------------------------------------------------------------------- /src/wikmd/plugins/mermaid/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/plugins/mermaid/__init__.py -------------------------------------------------------------------------------- /src/wikmd/plugins/mermaid/mermaid.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from wikmd.config import WikmdConfig 4 | 5 | injected_script = """ 6 | <script> 7 | mermaid.initialize({startOnLoad:true}); 8 | document.querySelectorAll('pre.mermaid, pre>code.language-mermaid').forEach($el => { 9 | if ($el.tagName === 'CODE') 10 | $el = $el.parentElement 11 | $el.outerHTML = ` 12 | <div class="mermaid">${$el.textContent}</div> 13 | ` 14 | }) 15 | </script> 16 | """ 17 | 18 | class Plugin: 19 | def import_head(self): 20 | return "<script src='" + self.web_dep["mermaid.min.js"] + "'></script>" 21 | 22 | def add_script(self): 23 | return injected_script 24 | 25 | def __init__(self, flask_app: Flask, config: WikmdConfig, web_dep ): 26 | self.name = "Mermaid integration" 27 | self.plugname = "mermaid" 28 | self.flask_app = flask_app 29 | self.config = config 30 | self.this_location = os.path.dirname(__file__) 31 | self.web_dep = web_dep 32 | 33 | def get_plugin_name(self) -> str: 34 | """ 35 | returns the name of the plugin 36 | """ 37 | return self.name 38 | -------------------------------------------------------------------------------- /src/wikmd/plugins/plantuml/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/plugins/plantuml/__init__.py -------------------------------------------------------------------------------- /src/wikmd/plugins/plantuml/plantuml.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import string 4 | 5 | from zlib import compress 6 | from base64 import b64encode 7 | from flask import Flask 8 | from wikmd.config import WikmdConfig 9 | 10 | 11 | class Plugin: 12 | 13 | def __init__(self, flask_app: Flask, config: WikmdConfig, web_dep): 14 | self.name = "plantuml" 15 | self.plugname = "plantuml" 16 | self.flask_app = flask_app 17 | self.config = config 18 | self.this_location = os.path.dirname(__file__) 19 | self.web_dep = web_dep 20 | 21 | plantuml_alphabet = string.digits + string.ascii_uppercase + string.ascii_lowercase + '-_' 22 | base64_alphabet = string.ascii_uppercase + string.ascii_lowercase + string.digits + '+/' 23 | self.b64_to_plantuml = bytes.maketrans(base64_alphabet.encode('utf-8'), plantuml_alphabet.encode('utf-8')) 24 | 25 | def get_plugin_name(self) -> str: 26 | """ 27 | returns the name of the plugin 28 | """ 29 | return self.name 30 | 31 | def process_md(self, md: str) -> str: 32 | """ 33 | returns the md file after process the input file 34 | """ 35 | return md 36 | 37 | def process_html(self, html: str) -> str: 38 | """ 39 | returns the html file after process the input file 40 | """ 41 | # replace all blocks [[plantuml (compressed_part)]] with img tags 42 | plantuml_blocks = re.findall(r"(\[\[plantuml(.*?)]])", html, re.DOTALL) 43 | for plantuml_block in plantuml_blocks: 44 | full_block, compressed_part = plantuml_block 45 | html = html.replace(full_block, self.get_img_tag(self.remove_redundant_symbols(compressed_part))) 46 | return html 47 | 48 | def process_md_before_html_convert(self, md: str) -> str: 49 | """ 50 | returns the md file after before html convertation 51 | """ 52 | plantuml_code_blocks = re.findall(r"(```plantuml(.*?)```)", md, re.DOTALL) 53 | for code_block in plantuml_code_blocks: 54 | full_block, code = code_block 55 | compressed_code = self.encode_plantuml(code) 56 | md = md.replace(full_block, "[[plantuml " + compressed_code + "]]") 57 | return md 58 | 59 | def request_html(self, get_html_callback): 60 | self.get_html = get_html_callback 61 | 62 | def get_img_tag(self, encoded_plantuml: str) -> str: 63 | """ 64 | returns the img tag for the given encoded plantuml 65 | """ 66 | return f'<br><img src="{self.config.plantuml_server_url}/png/{encoded_plantuml}"><br>' 67 | 68 | def encode_plantuml(self, plantuml_text: str) -> str: 69 | """ 70 | Compresses the plantuml code and encodes it in base64. 71 | """ 72 | compressed_string = compress(plantuml_text.encode('utf-8'))[2:-4] 73 | return b64encode(compressed_string).translate(self.b64_to_plantuml).decode('utf-8') 74 | 75 | def remove_redundant_symbols(self, s: str) -> str: 76 | return s.replace("\n", "").replace("\r", "").replace(" ", "") 77 | -------------------------------------------------------------------------------- /src/wikmd/plugins/swagger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/plugins/swagger/__init__.py -------------------------------------------------------------------------------- /src/wikmd/plugins/swagger/swagger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from flask import Flask 5 | from wikmd.config import WikmdConfig 6 | 7 | 8 | class Plugin: 9 | def import_head(self): 10 | return '<link rel="stylesheet" href="' + self.web_dep['swagger-ui.css'] + '" />' 11 | 12 | def __init__(self, flask_app: Flask, config: WikmdConfig, web_dep): 13 | self.name = "swagger" 14 | self.plugname = "swagger" 15 | self.flask_app = flask_app 16 | self.config = config 17 | self.this_location = os.path.dirname(__file__) 18 | self.web_dep = web_dep 19 | self.injected_html = f""" 20 | <div id='{{id}}', style=\"--bg-codeblock-light: rgba(0, 0, 0, 0);\"></div> 21 | <script src=\"{self.web_dep['swagger-ui-bundle.js']}\" crossorigin></script> 22 | <script> 23 | window.onload = () => {{ 24 | window.ui = SwaggerUIBundle({{ 25 | url: '{{url}}', 26 | dom_id: '#{{id}}', 27 | }}); 28 | }}; 29 | </script> 30 | """ 31 | 32 | def get_plugin_name(self) -> str: 33 | """ 34 | returns the name of the plugin 35 | """ 36 | return self.name 37 | 38 | def process_md(self, md: str) -> str: 39 | """ 40 | returns the md file after process the input file 41 | """ 42 | return md 43 | 44 | def process_html(self, html: str) -> str: 45 | """ 46 | returns the html file after process the input file 47 | """ 48 | return self.insert_swagger_divs(html) 49 | 50 | def request_html(self, get_html_callback): 51 | self.get_html = get_html_callback 52 | 53 | def insert_swagger_divs(self, file: str) -> str: 54 | """ 55 | inserts the swagger divs into the html file 56 | """ 57 | annotations = re.findall(r"(\[\[" + self.name + r".*?]])", file, re.DOTALL) 58 | result = file 59 | for i, annotation in enumerate(annotations): 60 | link_start = annotation.find("http") 61 | if link_start != -1: 62 | link = annotation[link_start:-2] 63 | result = re.sub(re.escape(annotation), self.prepare_html_string(link, i), result, count=1) 64 | return result 65 | 66 | def prepare_html_string(self, link: str, id: int) -> str: 67 | """ 68 | returns the html string with id and url 69 | """ 70 | return self.injected_html.replace("{id}", "swagger-ui-div-" + str(id)).replace("{url}", link) 71 | 72 | -------------------------------------------------------------------------------- /src/wikmd/search.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from collections import namedtuple 4 | from multiprocessing import Process 5 | from pathlib import Path 6 | from typing import List, NamedTuple, Tuple 7 | 8 | from bs4 import BeautifulSoup 9 | from markdown import Markdown 10 | from watchdog.events import FileSystemEvent, FileSystemEventHandler 11 | from watchdog.observers import Observer 12 | from whoosh import index, query 13 | from whoosh.fields import ID, TEXT, SchemaClass 14 | from whoosh.highlight import SentenceFragmenter 15 | from whoosh.qparser import MultifieldParser 16 | from whoosh.writing import AsyncWriter 17 | 18 | 19 | class SearchSchema(SchemaClass): 20 | path: ID = ID(stored=True, unique=True) 21 | filename: ID = ID(stored=True) 22 | title: TEXT = TEXT(stored=True) 23 | content: TEXT = TEXT(stored=True) 24 | 25 | 26 | SearchResult = namedtuple("Result", "path filename title score highlights") 27 | 28 | 29 | class Search: 30 | _index: index 31 | _schema: SearchSchema 32 | 33 | def __init__(self, index_path: str, create: bool = False): 34 | self._schema = SearchSchema() 35 | if create: 36 | if not os.path.exists(index_path): 37 | os.makedirs(index_path) 38 | self._index = index.create_in(index_path, self._schema) 39 | else: 40 | self._index = index.open_dir(index_path) 41 | 42 | def textify(self, text: str) -> str: 43 | md = Markdown(extensions=["meta", "extra"]) 44 | html = md.convert(text) 45 | soup = BeautifulSoup(html, "html.parser") 46 | return soup.get_text() 47 | 48 | def search( 49 | self, term: str, page: int 50 | ) -> Tuple[List[NamedTuple], int, int, List[str]]: 51 | query = MultifieldParser(["title", "content"], schema=self._schema).parse(term) 52 | frag = SentenceFragmenter(maxchars=2000) 53 | with self._index.searcher() as searcher: 54 | res = searcher.search_page(query, page) 55 | res.fragmenter = frag 56 | results = [ 57 | SearchResult( 58 | r.get("path"), 59 | r.get("filename"), 60 | r.get("title"), 61 | r.score, 62 | r.highlights("content"), 63 | ) 64 | for r in res 65 | ] 66 | corrector = searcher.corrector("content") 67 | suggestions = corrector.suggest(term) 68 | return results, res.total, res.pagecount, suggestions 69 | 70 | def index(self, path: str, filename: str, title: str, content: str): 71 | writer = AsyncWriter(self._index) 72 | content = self.textify(content) 73 | writer.add_document(path=path, filename=filename, title=title, content=content) 74 | writer.commit() 75 | 76 | def delete(self, path: str, filename: str): 77 | writer = AsyncWriter(self._index) 78 | q = query.And([query.Term("path", path), query.Term("filename", filename)]) 79 | writer.delete_by_query(q) 80 | writer.commit() 81 | 82 | def index_all(self, wiki_directory: str, files: List[Tuple[str, str, str]]): 83 | writer = AsyncWriter(self._index) 84 | for path, title, relpath in files: 85 | fpath = os.path.join(wiki_directory, relpath, path) 86 | with open(fpath, encoding="utf8") as f: 87 | content = f.read() 88 | content = self.textify(content) 89 | writer.add_document( 90 | path=relpath, filename=path, title=title, content=content 91 | ) 92 | writer.commit() 93 | 94 | def close(self): 95 | self._index.close() 96 | 97 | 98 | class Watchdog(FileSystemEventHandler): 99 | wiki_directory: str 100 | search_directory: str 101 | search: Search 102 | proc: Process 103 | 104 | def __init__(self, wiki_directory: str, search_directory: str): 105 | self.wiki_directory = Path(wiki_directory).absolute() 106 | self.search_directory = search_directory 107 | self.search = Search(self.search_directory) 108 | 109 | def rel_path(self, path: str): 110 | base_path = Path(path) 111 | rel_path = base_path.relative_to(self.wiki_directory) 112 | if len(rel_path.parts) == 0: 113 | return "." 114 | else: 115 | return str(rel_path) 116 | 117 | def on_created(self, event: FileSystemEvent): 118 | # Switch to dest_path if it exists because it means move 119 | file_path = event.src_path 120 | if hasattr(event, "dest_path"): 121 | file_path = event.dest_path 122 | 123 | if not os.path.splitext(file_path)[1].lower() == ".md": 124 | return 125 | 126 | base_path, filename = os.path.split(file_path) 127 | rel_path = self.rel_path(base_path) 128 | title, _ = os.path.splitext(filename) 129 | with open(file_path, encoding="utf8") as f: 130 | content = f.read() 131 | self.search.index(rel_path, filename, title, content) 132 | 133 | def on_deleted(self, event: FileSystemEvent): 134 | if not os.path.splitext(event.src_path)[1].lower() == ".md": 135 | return 136 | 137 | base_path, filename = os.path.split(event.src_path) 138 | rel_path = self.rel_path(base_path) 139 | self.search.delete(rel_path, filename) 140 | 141 | def on_moved(self, event: FileSystemEvent): 142 | self.on_deleted(event) 143 | self.on_created(event) 144 | 145 | def on_modified(self, event: FileSystemEvent): 146 | self.on_deleted(event) 147 | self.on_created(event) 148 | 149 | def watchdog(self): 150 | observer = Observer() 151 | observer.schedule(self, self.wiki_directory, recursive=True) 152 | observer.start() 153 | try: 154 | while observer.is_alive(): 155 | observer.join(1) 156 | time.sleep(1) 157 | finally: 158 | observer.stop() 159 | observer.join() 160 | 161 | def start(self): 162 | try: 163 | self.proc = Process(target=self.watchdog) 164 | self.proc.daemon = True 165 | self.proc.start() 166 | except KeyboardInterrupt: 167 | self.proc.terminate() 168 | 169 | def stop(self): 170 | if self.proc.is_alive(): 171 | self.proc.terminate() 172 | -------------------------------------------------------------------------------- /src/wikmd/static/css/codemirror.custom.css: -------------------------------------------------------------------------------- 1 | @import "wiki.colors.css"; 2 | 3 | .CodeMirror { background: var(--bg-codeblock-light); height: auto; padding: 6px; margin-bottom: 6px; border-radius: 6px; } 4 | .CodeMirror-gutters { background: var(--bg-codeblock-light); padding-right: 6px; } -------------------------------------------------------------------------------- /src/wikmd/static/css/codemirror.custom.dark.css: -------------------------------------------------------------------------------- 1 | /* Based on https://github.com/dempfi/ayu ayu-mirage */ 2 | @import "wiki.colors.css"; 3 | 4 | .cm-s-ayu-mirage.CodeMirror { background: var(--bg-codeblock-dark); color: #cbccc6; } 5 | .cm-s-ayu-mirage div.CodeMirror-selected { background: #34455a; } 6 | .cm-s-ayu-mirage .CodeMirror-line::selection, .cm-s-ayu-mirage .CodeMirror-line > span::selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::selection { background: #34455a; } 7 | .cm-s-ayu-mirage .CodeMirror-line::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::-moz-selection { background: rgba(25, 30, 42, 99); } 8 | 9 | .cm-s-ayu-mirage .CodeMirror-gutters { background: var(--bg-codeblock-dark); } 10 | .cm-s-ayu-mirage .CodeMirror-guttermarker { color: #ff0000; } 11 | .cm-s-ayu-mirage .CodeMirror-guttermarker-subtle { color: rgba(112, 122, 140, 66); } 12 | .cm-s-ayu-mirage .CodeMirror-linenumber { color: var(--linenumber-dark-color); } 13 | .cm-s-ayu-mirage .CodeMirror-cursor { border-left: 1px solid #ffcc66; } 14 | .cm-s-ayu-mirage.cm-fat-cursor .CodeMirror-cursor {background-color: #a2a8a175 !important;} 15 | .cm-s-ayu-mirage .cm-animate-fat-cursor { background-color: #a2a8a175 !important; } 16 | 17 | .cm-s-ayu-mirage span.cm-comment { color: #fffeb9; font-style:italic; } 18 | .cm-s-ayu-mirage span.cm-atom { color: #ae81ff; } 19 | .cm-s-ayu-mirage span.cm-number { color: #ffcc66; } 20 | 21 | .cm-s-ayu-mirage span.cm-comment.cm-attribute { color: #ffd580; } 22 | .cm-s-ayu-mirage span.cm-comment.cm-def { color: #d4bfff; } 23 | .cm-s-ayu-mirage span.cm-comment.cm-tag { color: #5ccfe6; } 24 | .cm-s-ayu-mirage span.cm-comment.cm-type { color: #5998a6; } 25 | 26 | .cm-s-ayu-mirage span.cm-property { color: #f29e74; } 27 | .cm-s-ayu-mirage span.cm-attribute { color: #ffd580; } 28 | .cm-s-ayu-mirage span.cm-keyword { color: #ffa759; } 29 | .cm-s-ayu-mirage span.cm-builtin { color: #ffcc66; } 30 | .cm-s-ayu-mirage span.cm-string { color: #bae67e; } 31 | 32 | .cm-s-ayu-mirage span.cm-variable { color: #cbccc6; } 33 | .cm-s-ayu-mirage span.cm-variable-2 { color: #f28779; } 34 | .cm-s-ayu-mirage span.cm-variable-3 { color: #5ccfe6; } 35 | .cm-s-ayu-mirage span.cm-type { color: #ffa759; } 36 | .cm-s-ayu-mirage span.cm-def { color: #ffd580; } 37 | .cm-s-ayu-mirage span.cm-bracket { color: rgba(92, 207, 230, 80); } 38 | .cm-s-ayu-mirage span.cm-tag { color: #5ccfe6; } 39 | .cm-s-ayu-mirage span.cm-header { color: #bae67e; } 40 | .cm-s-ayu-mirage span.cm-link { color: #5ccfe6; } 41 | .cm-s-ayu-mirage span.cm-error { color: #ff3333; } 42 | 43 | .cm-s-ayu-mirage .CodeMirror-activeline-background { background: #191e2a; } 44 | .cm-s-ayu-mirage .CodeMirror-matchingbracket { 45 | text-decoration: underline; 46 | color: white !important; 47 | } 48 | -------------------------------------------------------------------------------- /src/wikmd/static/css/filepond.dark.css: -------------------------------------------------------------------------------- 1 | @import "wiki.colors.css"; 2 | 3 | .filepond--panel-root { background: var(--bg-filepond-dark) } 4 | .filepond--drop-label { color: var(--txt-dark) } -------------------------------------------------------------------------------- /src/wikmd/static/css/wiki.colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Backgrounds */ 3 | --bg-light: white; 4 | /* old colors --bg-dark: #181a1c; */ 5 | --bg-dark: #222529; 6 | 7 | --html-integration-background-light: #e3e3e3; 8 | --html-integration-background-dark: #2f2f2f; 9 | 10 | --txt-light: #212529; 11 | --txt-dark: whitesmoke; 12 | 13 | --bg-codeblock-light: whitesmoke; 14 | /* old colors --bg-codeblock-dark: #27272a;*/ 15 | --bg-codeblock-dark: #1c1c1c; 16 | 17 | --bg-modal-light: white; 18 | --bg-modal-dark: #343a40; 19 | 20 | --quotes-color: orange; 21 | 22 | /* CodeMirror */ 23 | --linenumber-dark-color: #77797c; 24 | 25 | /* FilePond */ 26 | --bg-filepond-dark: #303033; 27 | 28 | /* Default primary */ 29 | --prmry-color: #b8daff; 30 | } 31 | -------------------------------------------------------------------------------- /src/wikmd/static/css/wiki.css: -------------------------------------------------------------------------------- 1 | @import "wiki.colors.css"; 2 | 3 | body { 4 | background-color: var(--bg-light); 5 | color: var(--txt-light); 6 | } 7 | 8 | .modal-content { 9 | background-color: var(--bg-modal-light); 10 | color: var(--txt-light); 11 | } 12 | 13 | blockquote { 14 | padding: 5px 50px; 15 | margin-left: 0px; 16 | border-left: 4px solid var(--quotes-color); 17 | background-color: var(--bg-codeblock-light); 18 | } 19 | 20 | blockquote p{ 21 | margin-top:10px; 22 | font-weight: 300; 23 | } 24 | 25 | code, pre { 26 | font-family: monospace; 27 | background-color: var(--bg-codeblock-light); 28 | color: #800; 29 | border-radius: 5px; 30 | padding: 4px; 31 | font-size: 14px 32 | } 33 | 34 | pre { 35 | background-color: var(--bg-codeblock-light); 36 | padding: 6px; 37 | border-radius: 6px; 38 | position: relative; 39 | } 40 | 41 | pre img { 42 | position: absolute; 43 | top: 5px; 44 | right: 5px; 45 | margin-top: 5px; 46 | margin-right: 5px; 47 | padding: 0.15rem; 48 | cursor: pointer; 49 | display: inline; 50 | } 51 | 52 | img { 53 | max-width: 100% 54 | } 55 | 56 | figure { 57 | text-align: center 58 | } 59 | 60 | .container-fluid { 61 | padding-right: 0; 62 | padding-left: 0; 63 | } 64 | 65 | .hljs { 66 | background-color: var(--bg-codeblock-light); 67 | } 68 | 69 | 70 | table { 71 | border-collapse: collapse; 72 | width: 100%; 73 | margin: 10px 0px; 74 | } 75 | 76 | th { 77 | padding-top: 12px; 78 | padding-bottom: 12px; 79 | text-align: left; 80 | border-bottom: 2px solid #dee2e6; 81 | } 82 | 83 | td { 84 | padding: 12px; 85 | border-top: 1px solid #dee2e6; 86 | } 87 | 88 | h1{ 89 | font-size: 2em; 90 | font-family: "quicksand"; 91 | } 92 | 93 | h1.title{ 94 | font-size: 2.5em; 95 | font-family: "Helvetica"; 96 | } 97 | 98 | h2 { 99 | font-size: 1.8em; 100 | font-family: "quicksand"; 101 | } 102 | 103 | h3 { 104 | font-size: 1.5em; 105 | font-family: "quicksand"; 106 | } 107 | 108 | 109 | h4{ 110 | font-size: 1.3em; 111 | font-family: "quicksand"; 112 | } 113 | 114 | 115 | .alert i[class^="bi-"]{ 116 | font-size: 1.5em; 117 | line-height: 1; 118 | margin-right: 10px; 119 | } 120 | 121 | div.html-integration{ 122 | background-color: var(--html-integration-background-light); 123 | padding: 20px; 124 | } 125 | 126 | 127 | .mermaid { 128 | text-align: center; 129 | } 130 | 131 | .footnote-back { 132 | margin-left: 0.5em; 133 | } 134 | -------------------------------------------------------------------------------- /src/wikmd/static/css/wiki.dark.css: -------------------------------------------------------------------------------- 1 | @import "wiki.colors.css"; 2 | 3 | body { 4 | background-color: var(--bg-dark); 5 | color: var(--txt-dark); 6 | } 7 | 8 | .modal-content { 9 | background-color: var(--bg-modal-dark); 10 | color: var(--txt-dark); 11 | } 12 | 13 | code, pre { 14 | background-color: var(--bg-codeblock-dark); 15 | color: #d88; 16 | } 17 | 18 | blockquote { 19 | padding: 5px 50px; 20 | margin-left: 0px; 21 | border-left: 4px solid var(--quotes-color); 22 | background-color: var(--bg-codeblock-dark); 23 | } 24 | 25 | blockquote p{ 26 | margin-top:10px; 27 | font-weight: 300; 28 | } 29 | 30 | pre { 31 | background-color: var(--bg-codeblock-dark); 32 | } 33 | 34 | div.html-integration{ 35 | background-color: var(--html-integration-background-dark) 36 | } 37 | 38 | .hljs { 39 | background-color: var(--bg-codeblock-dark); 40 | } 41 | 42 | .form-control, 43 | .form-control:focus { 44 | background-color: var(--bg-codeblock-dark); 45 | border-color: var(--bg-modal-dark); 46 | color: var(--txt-dark); 47 | } 48 | 49 | .list-group-item, 50 | .list-group-item { 51 | background-color: var(--bg-dark); 52 | color: var(--txt-dark); 53 | } 54 | 55 | .link-dark:visited, 56 | .link-dark:hover, 57 | .link-dark { 58 | color: var(--txt-dark); 59 | } -------------------------------------------------------------------------------- /src/wikmd/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/static/favicon.ico -------------------------------------------------------------------------------- /src/wikmd/static/fonts/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/static/fonts/.empty -------------------------------------------------------------------------------- /src/wikmd/static/images/contrast-2-fill.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-6.671-5.575A8 8 0 1 0 16.425 5.328a8.997 8.997 0 0 1-2.304 8.793 8.997 8.997 0 0 1-8.792 2.304z" fill="rgba(255,255,255,1)"/></svg> -------------------------------------------------------------------------------- /src/wikmd/static/images/file-copy-white.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.007-1H7zM5.003 8L5 20h10V8H5.003zM9 6h8v10h2V4H9v2z" fill="rgba(255,255,255,1)"/></svg> -------------------------------------------------------------------------------- /src/wikmd/static/images/file-copy.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.007-1H7zM5.003 8L5 20h10V8H5.003zM9 6h8v10h2V4H9v2z"/></svg> -------------------------------------------------------------------------------- /src/wikmd/static/images/graph_5.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | version="1.1" 13 | id="Layer_1" 14 | x="0px" 15 | y="0px" 16 | viewBox="0 0 512 512" 17 | style="enable-background:new 0 0 512 512;" 18 | xml:space="preserve" 19 | sodipodi:docname="graph_5.svg" 20 | inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata 21 | id="metadata873"><rdf:RDF><cc:Work 22 | rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type 23 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs 24 | id="defs871" /><sodipodi:namedview 25 | pagecolor="#ffffff" 26 | bordercolor="#666666" 27 | borderopacity="1" 28 | objecttolerance="10" 29 | gridtolerance="10" 30 | guidetolerance="10" 31 | inkscape:pageopacity="0" 32 | inkscape:pageshadow="2" 33 | inkscape:window-width="1848" 34 | inkscape:window-height="1030" 35 | id="namedview869" 36 | showgrid="false" 37 | inkscape:zoom="1.6542969" 38 | inkscape:cx="256" 39 | inkscape:cy="256" 40 | inkscape:window-x="72" 41 | inkscape:window-y="27" 42 | inkscape:window-maximized="1" 43 | inkscape:current-layer="g866" /> 44 | <g 45 | id="g866"> 46 | <path 47 | d="M443.7,12.8L443.7,12.8c-37.7,0-68.2,30.5-68.2,68.2c0,24.1,12.5,45.3,31.3,57.4l0.3,0.2l-44.5,155.9l-4.2-0.1h-0.1 c-15.3,0-29.5,5.1-40.8,13.6l0.2-0.1l-82.3-68.5c2.3-6.5,3.6-14,3.6-21.8c0-37.7-30.5-68.2-68.2-68.2c-37.7,0-68.2,30.5-68.2,68.2 c0,19.1,7.9,36.4,20.5,48.8l0,0l-48.3,96.6c-2.1-0.2-4.2-0.3-6.4-0.3h-0.1C30.6,362.6,0,393.1,0,430.9s30.6,68.3,68.3,68.3 s68.3-30.6,68.3-68.3c0-23.2-11.6-43.8-29.4-56.1l-0.2-0.1l45.6-91.2c5.4,1.5,11.7,2.5,18.1,2.5c17.7,0,33.8-6.8,45.9-17.8l0,0 l79.7,66.4c-3.8,8.3-6.1,18-6.1,28.2c0,37.7,30.6,68.2,68.2,68.2c37.7,0,68.2-30.5,68.2-68.2c0-24.1-12.5-45.3-31.3-57.4l-0.3-0.2 l44.5-155.9l4.2,0.1c37.7,0,68.2-30.6,68.2-68.2C512,43.4,481.5,12.9,443.7,12.8L443.7,12.8z M68.4,464.9 c-18.8,0-34.1-15.3-34.1-34.1s15.3-34.1,34.1-34.1s34.1,15.3,34.1,34.1C102.5,449.6,87.3,464.8,68.4,464.9z M170.8,251.6 c-18.8,0-34.1-15.3-34.1-34.1s15.3-34.1,34.1-34.1s34.1,15.3,34.1,34.1C204.9,236.3,189.6,251.6,170.8,251.6z M358.4,396.6 c-18.8,0-34.1-15.3-34.1-34.1s15.3-34.1,34.1-34.1s34.1,15.3,34.1,34.1C392.5,381.3,377.3,396.6,358.4,396.6z M443.7,115.2 c-18.8,0-34.1-15.3-34.1-34.1s15.3-34.1,34.1-34.1c18.8,0,34.1,15.3,34.1,34.1C477.8,99.9,462.6,115.1,443.7,115.2z" 48 | id="path864" 49 | style="fill:#ffffff" /> 50 | </g> 51 | </svg> -------------------------------------------------------------------------------- /src/wikmd/static/images/knowledge-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/static/images/knowledge-graph.png -------------------------------------------------------------------------------- /src/wikmd/static/images/readme-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/static/images/readme-img.png -------------------------------------------------------------------------------- /src/wikmd/static/images/wiki.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/static/images/wiki.gif -------------------------------------------------------------------------------- /src/wikmd/static/images/wiki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/static/images/wiki.png -------------------------------------------------------------------------------- /src/wikmd/static/images/wiki2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/static/images/wiki2.gif -------------------------------------------------------------------------------- /src/wikmd/static/js/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linbreux/wikmd/6d2c7457f47394696cbbf3433d78e8a193446a2e/src/wikmd/static/js/.empty -------------------------------------------------------------------------------- /src/wikmd/static/js/copypaste.js: -------------------------------------------------------------------------------- 1 | const notyf = new Notyf({duration: 2000, ripple: false, position: { x: 'center', y: 'top' }}); 2 | var codeblocks = document.getElementsByTagName("PRE"); 3 | var block; 4 | 5 | const pathToCopyIcon = darktheme == "False" ? "file-copy.svg" : "file-copy-white.svg" 6 | console.log(darktheme) 7 | 8 | for (block = 0; block < codeblocks.length; block++) { 9 | codeblocks[block].innerHTML += 10 | "<img id=\"copybutton" 11 | + block 12 | + "\" onclick=\"copyCodeBlock(" 13 | + block 14 | + ")\" title=\"Copy to clipboard\" src=\"/static/images/" + pathToCopyIcon + "\"" 15 | + "></img>" 16 | } 17 | 18 | function copyCodeBlock() { 19 | var codeBlock = document.getElementById("copybutton" + arguments[0]).parentNode.children[0].textContent; 20 | var dummy = document.createElement("textarea"); 21 | document.body.appendChild(dummy); 22 | dummy.value = codeBlock; 23 | dummy.select(); 24 | document.execCommand("copy"); 25 | document.body.removeChild(dummy); 26 | notyf.success('Copied'); 27 | } 28 | -------------------------------------------------------------------------------- /src/wikmd/static/js/drawio.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2006-2020, JGraph Ltd 3 | * Copyright (c) 2006-2020, Gaudenz Alder 4 | * 5 | * Usage: DiagramEditor.editElement(elt) where elt is an img or object with 6 | * a data URI src or data attribute or an svg node with a content attribute. 7 | * 8 | * See https://jgraph.github.io/drawio-integration/javascript.html 9 | */ 10 | function DiagramEditor(config, ui, done, initialized, urlParams) 11 | { 12 | this.config = (config != null) ? config : this.config; 13 | this.ui = (ui != null) ? ui : this.ui; 14 | this.done = (done != null) ? done : this.done; 15 | this.initialized = (initialized != null) ? initialized : this.initialized; 16 | this.urlParams = urlParams; 17 | 18 | var self = this; 19 | 20 | this.handleMessageEvent = function(evt) 21 | { 22 | if (self.frame != null && evt.source == self.frame.contentWindow && 23 | evt.data.length > 0) 24 | { 25 | try 26 | { 27 | var msg = JSON.parse(evt.data); 28 | 29 | if (msg != null) 30 | { 31 | self.handleMessage(msg); 32 | } 33 | } 34 | catch (e) 35 | { 36 | console.error(e); 37 | } 38 | } 39 | }; 40 | }; 41 | 42 | /** 43 | * Static method to edit the diagram in the given img or object. 44 | */ 45 | DiagramEditor.editElement = function(elt, config, ui, done, urlParams) 46 | { 47 | if (!elt.diagramEditorStarting) 48 | { 49 | elt.diagramEditorStarting = true; 50 | 51 | return new DiagramEditor(config, ui, done, function() 52 | { 53 | delete elt.diagramEditorStarting; 54 | }, urlParams).editElement(elt); 55 | } 56 | }; 57 | 58 | /** 59 | * Global configuration. 60 | */ 61 | DiagramEditor.prototype.config = null; 62 | 63 | /** 64 | * Protocol and domain to use. 65 | */ 66 | DiagramEditor.prototype.drawDomain = 'https://embed.diagrams.net/'; 67 | 68 | /** 69 | * UI theme to be use. 70 | */ 71 | DiagramEditor.prototype.ui = 'min'; 72 | 73 | /** 74 | * Contains XML for pending image export. 75 | */ 76 | DiagramEditor.prototype.xml = null; 77 | 78 | /** 79 | * Format to use. 80 | */ 81 | DiagramEditor.prototype.format = 'xml'; 82 | 83 | /** 84 | * Specifies if libraries should be enabled. 85 | */ 86 | DiagramEditor.prototype.libraries = true; 87 | 88 | /** 89 | * CSS style for the iframe. 90 | */ 91 | //DiagramEditor.prototype.frameStyle = 'position:absolute;border:0;width:100%;height:100%;'; 92 | 93 | DiagramEditor.prototype.frameStyle = 'position:fixed; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;' 94 | 95 | /** 96 | * Adds the iframe and starts editing. 97 | */ 98 | DiagramEditor.prototype.editElement = function(elem) 99 | { 100 | var src = this.getElementData(elem); 101 | this.startElement = elem; 102 | var fmt = this.format; 103 | 104 | if (src.substring(0, 15) === 'data:image/png;') 105 | { 106 | fmt = 'xmlpng'; 107 | } 108 | else if (src.substring(0, 19) === 'data:image/svg+xml;' || 109 | elem.nodeName.toLowerCase() == 'svg') 110 | { 111 | fmt = 'xmlsvg'; 112 | } 113 | 114 | this.startEditing(src, fmt); 115 | 116 | return this; 117 | }; 118 | 119 | /** 120 | * Adds the iframe and starts editing. 121 | */ 122 | DiagramEditor.prototype.getElementData = function(elem) 123 | { 124 | var name = elem.nodeName.toLowerCase(); 125 | 126 | return elem.getAttribute((name == 'svg') ? 'content' : 127 | ((name == 'img') ? 'src' : 'data')); 128 | }; 129 | 130 | /** 131 | * Sets image 132 | */ 133 | DiagramEditor.prototype.setElementData = function(elem, data) 134 | { 135 | var name = elem.nodeName.toLowerCase(); 136 | 137 | if (name == 'svg') 138 | { 139 | elem.outerHTML = atob(data.substring(data.indexOf(',') + 1)); 140 | } 141 | else 142 | { 143 | elem.setAttribute((name == 'img') ? 'src' : 'data', data); 144 | } 145 | 146 | const form = document.createElement('form'); 147 | form.method = "post"; 148 | form.action = "/plug_com"; 149 | 150 | 151 | var object = { 152 | "id": elem.getAttribute("id"), 153 | "image": elem.outerHTML, 154 | } 155 | 156 | for (const key in object) { 157 | if (object.hasOwnProperty(key)) { 158 | const hiddenField = document.createElement('input'); 159 | hiddenField.type = 'hidden'; 160 | hiddenField.name = key; 161 | hiddenField.value = object[key]; 162 | 163 | form.appendChild(hiddenField); 164 | } 165 | } 166 | var data = new FormData(form); 167 | document.body.appendChild(form); 168 | 169 | var xhr = new XMLHttpRequest(); 170 | 171 | xhr.open("POST",form.action,true) 172 | xhr.send(data) 173 | 174 | 175 | console.log(elem.getAttribute("id")) 176 | return elem; 177 | }; 178 | 179 | /** 180 | * Starts the editor for the given data. 181 | */ 182 | DiagramEditor.prototype.startEditing = function(data, format, title) 183 | { 184 | if (this.frame == null) 185 | { 186 | window.addEventListener('message', this.handleMessageEvent); 187 | this.format = (format != null) ? format : this.format; 188 | this.title = (title != null) ? title : this.title; 189 | this.data = data; 190 | 191 | this.frame = this.createFrame( 192 | this.getFrameUrl(), 193 | this.getFrameStyle()); 194 | document.body.appendChild(this.frame); 195 | this.setWaiting(true); 196 | } 197 | }; 198 | 199 | /** 200 | * Updates the waiting cursor. 201 | */ 202 | DiagramEditor.prototype.setWaiting = function(waiting) 203 | { 204 | if (this.startElement != null) 205 | { 206 | // Redirect cursor to parent for SVG and object 207 | var elt = this.startElement; 208 | var name = elt.nodeName.toLowerCase(); 209 | 210 | if (name == 'svg' || name == 'object') 211 | { 212 | elt = elt.parentNode; 213 | } 214 | 215 | if (elt != null) 216 | { 217 | if (waiting) 218 | { 219 | this.frame.style.pointerEvents = 'none'; 220 | this.previousCursor = elt.style.cursor; 221 | elt.style.cursor = 'wait'; 222 | } 223 | else 224 | { 225 | elt.style.cursor = this.previousCursor; 226 | this.frame.style.pointerEvents = ''; 227 | } 228 | } 229 | } 230 | }; 231 | 232 | /** 233 | * Updates the waiting cursor. 234 | */ 235 | DiagramEditor.prototype.setActive = function(active) 236 | { 237 | if (active) 238 | { 239 | this.previousOverflow = document.body.style.overflow; 240 | document.body.style.overflow = 'hidden'; 241 | } 242 | else 243 | { 244 | document.body.style.overflow = this.previousOverflow; 245 | } 246 | }; 247 | 248 | /** 249 | * Removes the iframe. 250 | */ 251 | DiagramEditor.prototype.stopEditing = function() 252 | { 253 | if (this.frame != null) 254 | { 255 | window.removeEventListener('message', this.handleMessageEvent); 256 | document.body.removeChild(this.frame); 257 | this.setActive(false); 258 | this.frame = null; 259 | } 260 | }; 261 | 262 | /** 263 | * Send the given message to the iframe. 264 | */ 265 | DiagramEditor.prototype.postMessage = function(msg) 266 | { 267 | if (this.frame != null) 268 | { 269 | this.frame.contentWindow.postMessage(JSON.stringify(msg), '*'); 270 | } 271 | }; 272 | 273 | /** 274 | * Returns the diagram data. 275 | */ 276 | DiagramEditor.prototype.getData = function() 277 | { 278 | return this.data; 279 | }; 280 | 281 | /** 282 | * Returns the title for the editor. 283 | */ 284 | DiagramEditor.prototype.getTitle = function() 285 | { 286 | return this.title; 287 | }; 288 | 289 | /** 290 | * Returns the CSS style for the iframe. 291 | */ 292 | DiagramEditor.prototype.getFrameStyle = function() 293 | { 294 | return this.frameStyle + ';left:' + 295 | document.body.scrollLeft + 'px;top:' + 296 | 70 + 'px;'; 297 | }; 298 | 299 | /** 300 | * Returns the URL for the iframe. 301 | */ 302 | DiagramEditor.prototype.getFrameUrl = function() 303 | { 304 | var url = this.drawDomain + '?proto=json&spin=1'; 305 | 306 | if (this.ui != null) 307 | { 308 | url += '&ui=' + this.ui; 309 | } 310 | 311 | if (this.libraries != null) 312 | { 313 | url += '&libraries=1'; 314 | } 315 | 316 | if (this.config != null) 317 | { 318 | url += '&configure=1'; 319 | } 320 | 321 | if (this.urlParams != null) 322 | { 323 | url += '&' + this.urlParams.join('&'); 324 | } 325 | 326 | return url; 327 | }; 328 | 329 | /** 330 | * Creates the iframe. 331 | */ 332 | DiagramEditor.prototype.createFrame = function(url, style) 333 | { 334 | var frame = document.createElement('iframe'); 335 | frame.setAttribute('frameborder', '0'); 336 | frame.setAttribute('style', style); 337 | frame.setAttribute('src', url); 338 | 339 | return frame; 340 | }; 341 | 342 | /** 343 | * Sets the status of the editor. 344 | */ 345 | DiagramEditor.prototype.setStatus = function(messageKey, modified) 346 | { 347 | this.postMessage({action: 'status', messageKey: messageKey, modified: modified}); 348 | }; 349 | 350 | /** 351 | * Handles the given message. 352 | */ 353 | DiagramEditor.prototype.handleMessage = function(msg) 354 | { 355 | if (msg.event == 'configure') 356 | { 357 | this.configureEditor(); 358 | } 359 | else if (msg.event == 'init') 360 | { 361 | this.initializeEditor(); 362 | } 363 | else if (msg.event == 'autosave') 364 | { 365 | this.save(msg.xml, true, this.startElement); 366 | } 367 | else if (msg.event == 'export') 368 | { 369 | this.setElementData(this.startElement, msg.data); 370 | this.stopEditing(); 371 | this.xml = null; 372 | } 373 | else if (msg.event == 'save') 374 | { 375 | this.save(msg.xml, false, this.startElement); 376 | this.xml = msg.xml; 377 | 378 | if (msg.exit) 379 | { 380 | msg.event = 'exit'; 381 | } 382 | else 383 | { 384 | this.setStatus('allChangesSaved', false); 385 | } 386 | } 387 | 388 | if (msg.event == 'exit') 389 | { 390 | if (this.format != 'xml') 391 | { 392 | if (this.xml != null) 393 | { 394 | this.postMessage({action: 'export', format: this.format, 395 | xml: this.xml, spinKey: 'export'}); 396 | } 397 | else 398 | { 399 | this.stopEditing(msg); 400 | } 401 | } 402 | else 403 | { 404 | if (msg.modified == null || msg.modified) 405 | { 406 | this.save(msg.xml, false, this.startElement); 407 | } 408 | 409 | this.stopEditing(msg); 410 | } 411 | } 412 | }; 413 | 414 | /** 415 | * Posts configure message to editor. 416 | */ 417 | DiagramEditor.prototype.configureEditor = function() 418 | { 419 | this.postMessage({action: 'configure', config: this.config}); 420 | }; 421 | 422 | /** 423 | * Posts load message to editor. 424 | */ 425 | DiagramEditor.prototype.initializeEditor = function() 426 | { 427 | this.postMessage({action: 'load',autosave: 1, saveAndExit: '1', 428 | modified: 'unsavedChanges', xml: this.getData(), 429 | title: this.getTitle()}); 430 | this.setWaiting(false); 431 | this.setActive(true); 432 | this.initialized(); 433 | }; 434 | 435 | /** 436 | * Saves the given data. 437 | */ 438 | DiagramEditor.prototype.save = function(data, draft, elt) 439 | { 440 | this.done(data, draft, elt); 441 | }; 442 | 443 | /** 444 | * Invoked after save. 445 | */ 446 | DiagramEditor.prototype.done = function() 447 | { 448 | // hook for subclassers 449 | }; 450 | 451 | /** 452 | * Invoked after the editor has sent the init message. 453 | */ 454 | DiagramEditor.prototype.initialized = function() 455 | { 456 | // hook for subclassers 457 | }; -------------------------------------------------------------------------------- /src/wikmd/templates/base.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | 4 | <head> 5 | <!-- Required meta tags --> 6 | <meta charset="utf-8"> 7 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 8 | 9 | <!-- CSS --> 10 | <link rel="stylesheet" type="text/css" href="{{ system.web_deps["bootstrap-icons.css"] }}"/> 11 | 12 | <!-- Bootstrap --> 13 | <link href="{{ system.web_deps["bootstrap.min.css"] }}" rel="stylesheet" 14 | integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" 15 | crossorigin="anonymous"> 16 | <!-- Highlight default/dark depending on the theme --> 17 | {% if system.darktheme == True %} 18 | <link rel="stylesheet" href="{{ system.web_deps["dark.min.css"] }}"> 19 | {% else %} 20 | <link rel="stylesheet" href="{{ system.web_deps["default.min.css"] }}"> 21 | {% endif %} 22 | <!-- wikmd custom --> 23 | <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/wiki.colors.css') }}"> 24 | <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/wiki.css') }}"> 25 | {% if system.darktheme == True %} 26 | <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/wiki.dark.css') }}"> 27 | {% endif %} 28 | {% block head %}{% endblock %} 29 | {% for plug in system.plugins %}{% if plug.import_head is defined %}{{plug.import_head()|safe}} 30 | {% endif%}{%endfor%} 31 | <!-- favicon --> 32 | <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> 33 | 34 | <!-- fonts --> 35 | <style> 36 | @font-face { 37 | font-family: 'Quicksand'; 38 | src: url('{{ system.web_deps["quicksand.woff2"] }}') format('woff2'); 39 | } 40 | </style> 41 | 42 | <title>{{ system.wiki_title }} 43 | 44 | 45 | 46 | 47 | 104 | {% with messages = get_flashed_messages() %} 105 | {% if messages %} 106 | 111 | {% endif %} 112 | {% endwith %} 113 | 114 | 115 |
116 | 117 | 118 |
119 | {% block content %} 120 | 121 | {% endblock %} 122 |
123 | 124 | 125 | 141 |
142 | 143 | 144 | 145 | 146 | 149 | 150 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | {% block scripts %} 162 | 163 | {% endblock %} 164 | 165 | {% for plug in system.plugins %}{% if plug.add_script is defined %}{{plug.add_script()|safe}}{% endif%}{%endfor%} 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /src/wikmd/templates/content.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head %} 4 | 5 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |

{% if folder %} 12 | {{ folder }}/ 14 | {% endif %} 15 | {{ title }} 16 | 17 | 24 | 25 | 26 | 33 | 34 | 35 |

36 | {{ info|safe }} 37 |
38 |

{{ modif }}

39 |
40 | 41 |
42 | 43 | 44 | 64 | 65 | {% endblock %} 66 | 67 | {% block scripts %} 68 | 69 | 70 | {% endblock %} 71 | -------------------------------------------------------------------------------- /src/wikmd/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {%block content%} 4 | 5 | 6 | 13 | 14 | 15 | {{homepage|safe}} 16 | 17 | {%endblock%} 18 | -------------------------------------------------------------------------------- /src/wikmd/templates/knowledge-graph.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {%block head%} 4 | 5 | 6 | 13 | {%endblock%} 14 | 15 | {%block content%} 16 | 17 |
18 | 19 | 79 | {%endblock%} 80 | -------------------------------------------------------------------------------- /src/wikmd/templates/list_files.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | Sort ({% if system.listsortMTime %}last modified{% else %}a-z{% endif%}) 5 |

{{ folder }} ALL FILES

6 |
    7 |
  • ..
  • 8 | {% for i in list %} 9 |
  • 10 | {% if i.folder != "" and i.folder != folder %} 11 | {{ i.folder }}/ 12 | {% else %} 13 | {{ i.doc }} 14 | {% endif %} 15 |
  • 16 | {% endfor %} 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /src/wikmd/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {%block content%} 4 | 5 |
6 |
7 | 8 |

Login

9 | You need password access to edit this page. 10 |
11 |
12 | 13 | 14 |
15 | 16 |
17 |
18 |
19 | {%endblock%} 20 | 21 | -------------------------------------------------------------------------------- /src/wikmd/templates/new.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {%block head%} 5 | 6 | 7 | 8 | 9 | 10 | {% if system.darktheme == True %} 11 | 12 | 13 | {% endif %} 14 | 15 | {%endblock%} 16 | 17 | 18 | {% block content %} 19 | 20 |
21 |
22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 |
30 | 31 |

32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 60 | 61 | 118 | {% endblock %} 119 | -------------------------------------------------------------------------------- /src/wikmd/templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head %} 4 | 58 | 59 | {% endblock %} 60 | 61 | {% block content %} 62 |

Found {{ num_results }} result(s) for '{{ search_term }}'

63 |

64 | Did you mean?: 65 | {% for term in suggestions %} 66 | {{ term }} 67 | {% if not loop.last %}, {% endif %} 68 | {% endfor %} 69 |

77 | {% if num_pages > 1 %} 78 |
    79 | {% for page in range(1, num_pages + 1) %} 80 | 83 | {% endfor %} 84 |
      85 | {% endif %} 86 | {% endblock %} 87 | -------------------------------------------------------------------------------- /src/wikmd/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unicodedata 3 | import re 4 | 5 | _filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9 _.-]") 6 | _windows_device_files = { 7 | "CON", 8 | "PRN", 9 | "AUX", 10 | "NUL", 11 | *(f"COM{i}" for i in range(10)), 12 | *(f"LPT{i}" for i in range(10)), 13 | } 14 | 15 | 16 | def secure_filename(filename: str) -> str: 17 | """Convert your filename to be safe for the os. 18 | 19 | Function from werkzeug. Changed to allow space in the file name. 20 | """ 21 | filename = unicodedata.normalize("NFKD", filename) 22 | filename = filename.encode("ascii", "ignore").decode("ascii") 23 | for sep in os.sep, os.path.altsep: 24 | if sep: 25 | filename = filename.replace(sep, "_") 26 | filename = str(_filename_ascii_strip_re.sub("", filename)).strip( 27 | "._" 28 | ) 29 | # on nt a couple of special files are present in each folder. We 30 | # have to ensure that the target file is not such a filename. In 31 | # this case we prepend an underline 32 | if ( 33 | os.name == "nt" 34 | and filename 35 | and filename.split(".")[0].upper() in _windows_device_files 36 | ): 37 | filename = f"_{filename}" 38 | 39 | return filename 40 | 41 | 42 | def pathify(path1, path2): 43 | """ 44 | Joins two paths and eventually converts them from Win (\\) to linux OS separator. 45 | :param path1: first path 46 | :param path2: second path 47 | :return safe joined path 48 | """ 49 | return os.path.join(path1, path2).replace("\\", "/") 50 | 51 | 52 | def move_all_files(src_dir: str, dest_dir: str): 53 | """ 54 | Function that moves all the files from a source directory to a destination one. 55 | If a file with the same name is already present in the destination, the source file will be renamed with a 56 | '-copy-XX' suffix. 57 | :param src_dir: source directory 58 | :param dest_dir: destination directory 59 | """ 60 | if not os.path.isdir(dest_dir): 61 | os.mkdir(dest_dir) # make the dir if it doesn't exist 62 | 63 | src_files = os.listdir(src_dir) 64 | dest_files = os.listdir(dest_dir) 65 | 66 | for file in src_files: 67 | new_file = file 68 | copies_count = 1 69 | while new_file in dest_files: # if the file is already present, append '-copy-XX' to the file name 70 | file_split = file.split('.') 71 | new_file = f"{file_split[0]}-copy-{copies_count}" 72 | if len(file_split) > 1: # if the file has an extension (it's not a directory nor a file without extension) 73 | new_file += f".{file_split[1]}" # add the extension 74 | copies_count += 1 75 | 76 | os.rename(f"{src_dir}/{file}", f"{dest_dir}/{new_file}") -------------------------------------------------------------------------------- /src/wikmd/web_dependencies.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from os import path, makedirs 3 | 4 | import requests 5 | 6 | WebDependency = namedtuple("WebDependency", ["local", "external"]) 7 | WEB_DEPENDENCIES = { 8 | "bootstrap.min.css": WebDependency( 9 | local="/static/css/bootstrap.min.css", 10 | external="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" 11 | ), 12 | "dark.min.css": WebDependency( 13 | local="/static/css/dark.min.css", 14 | external="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/dark.min.css" 15 | ), 16 | "default.min.css": WebDependency( 17 | local="/static/css/default.min.css", 18 | external="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/default.min.css" 19 | ), 20 | "bootstrap.bundle.min.js": WebDependency( 21 | local="/static/js/bootstrap.bundle.min.js", 22 | external="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" 23 | ), 24 | "bootstrap-icons.css": WebDependency( 25 | local="/static/css/bootstrap-icons.css", 26 | external="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" 27 | ), 28 | "bootstrap-icons.woff": WebDependency( 29 | local="/static/css/fonts/bootstrap-icons.woff", 30 | external="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/fonts/bootstrap-icons.woff" 31 | ), 32 | "bootstrap-icons.woff2": WebDependency( 33 | local="/static/css/fonts/bootstrap-icons.woff2", 34 | external="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/fonts/bootstrap-icons.woff2" 35 | ), 36 | "jquery.slim.min.js": WebDependency( 37 | local="/static/js/jquery.slim.min.js", 38 | external="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.slim.min.js" 39 | ), 40 | "polyfill.min.js": WebDependency( 41 | local="/static/js/polyfill.min.js", 42 | external="https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=es6" 43 | ), 44 | "tex-mml-chtml.js": WebDependency( 45 | local="/static/js/tex-mml-chtml.js", 46 | external="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" 47 | ), 48 | "highlight.min.js": WebDependency( 49 | local="/static/js/highlight.min.js", 50 | external="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/highlight.min.js" 51 | ), 52 | "codemirror.min.css": WebDependency( 53 | local="/static/css/codemirror.min.css", 54 | external="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.52.2/codemirror.min.css" 55 | ), 56 | "codemirror.min.js": WebDependency( 57 | local="/static/js/codemirror.min.js", 58 | external="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.52.2/codemirror.min.js" 59 | ), 60 | "markdown.min.js": WebDependency( 61 | local="/static/js/markdown.min.js", 62 | external="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.52.2/mode/markdown/markdown.min.js" 63 | ), 64 | "vis-network.min.js": WebDependency( 65 | local="/static/js/vis-network.min.js", 66 | external="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js" 67 | ), 68 | "filepond.js": WebDependency( 69 | local="/static/js/filepond.js", 70 | external="https://unpkg.com/filepond/dist/filepond.js" 71 | ), 72 | "filepond.css": WebDependency( 73 | local="/static/css/filepond.css", 74 | external="https://unpkg.com/filepond/dist/filepond.css" 75 | ), 76 | "filepond-plugin-file-validate-type.js": WebDependency( 77 | local="/static/js/filepond-plugin-file-validate-type.js", 78 | external="https://unpkg.com/filepond-plugin-file-validate-type@1.2.8/dist/filepond-plugin-file-validate-type.js" 79 | ), 80 | "notyf.min.js": WebDependency( 81 | local="/static/js/notyf.min.js", 82 | external="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.js" 83 | ), 84 | "notyf.min.css": WebDependency( 85 | local="/static/css/notyf.min.css", 86 | external="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.css" 87 | ), 88 | "mermaid.min.js": WebDependency( 89 | local="/static/js/mermaid.min.js", 90 | external="https://cdn.jsdelivr.net/npm/mermaid@9.3.0/dist/mermaid.min.js" 91 | ), 92 | "quicksand.woff2": WebDependency( 93 | local="/static/fonts/quicksand.woff2", 94 | external="https://fonts.gstatic.com/s/quicksand/v31/6xKtdSZaM9iE8KbpRA_hK1QN.woff2" 95 | ), 96 | "swagger-ui-bundle.js": WebDependency( 97 | local="/static/js/swagger-ui-bundle.js", 98 | external="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js" 99 | ), 100 | "swagger-ui.css": WebDependency( 101 | local="/static/css/swagger-ui.css", 102 | external="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui.css" 103 | ) 104 | } 105 | 106 | 107 | def get_web_deps(local_mode, logger): 108 | """ 109 | Returns a dict with dependency_name as key and web_path as value. 110 | If local_mode: a local copy of the dependency gets served 111 | Else: a public CDNs gets used to serve those dependencies 112 | """ 113 | if local_mode: 114 | download_web_deps(logger) 115 | return {dep: WEB_DEPENDENCIES[dep].local for dep in WEB_DEPENDENCIES} 116 | else: 117 | return {dep: WEB_DEPENDENCIES[dep].external for dep in WEB_DEPENDENCIES} 118 | 119 | 120 | def download_web_deps(logger): 121 | """ 122 | Downloads the dependencies, if they don't already exist on disk 123 | """ 124 | for dep_name in WEB_DEPENDENCIES: 125 | dep = WEB_DEPENDENCIES[dep_name] 126 | dep_file_path = path.join(path.dirname(__file__), dep.local[1:]) # Drop the first '/' so join works 127 | dep_folder_path = path.dirname(dep_file_path) 128 | 129 | # Dependency parent folder is not present and has to be created 130 | if not path.exists(dep_folder_path): 131 | logger.info(f"Creating dependency folder {dep_folder_path}") 132 | makedirs(dep_folder_path) 133 | 134 | # File is not present and has to be downloaded 135 | if not path.exists(dep_file_path): 136 | logger.info(f"Downloading dependency {dep.external}") 137 | result = requests.get(dep.external) 138 | if not result.ok: 139 | raise Exception(f"Error while trying to GET {dep.external} Statuscode: {result.status_code}") 140 | 141 | logger.info(f"Writing dependency >>> {dep_file_path}") 142 | with open(dep_file_path, "wb") as file: 143 | file.write(result.content) 144 | -------------------------------------------------------------------------------- /src/wikmd/wiki.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import secrets 4 | import time 5 | import uuid 6 | from hashlib import sha256 7 | from os.path import exists 8 | from threading import Thread 9 | import shutil 10 | from pathlib import Path 11 | 12 | import pypandoc 13 | from flask import ( 14 | Flask, 15 | flash, 16 | make_response, 17 | redirect, 18 | render_template, 19 | request, 20 | send_file, 21 | send_from_directory, 22 | url_for, 23 | ) 24 | from lxml.html.clean import clean_html 25 | from werkzeug.utils import safe_join 26 | from wikmd import knowledge_graph 27 | from wikmd.cache import Cache 28 | from wikmd.config import WikmdConfig 29 | from wikmd.git_manager import WikiRepoManager 30 | from wikmd.image_manager import ImageManager 31 | from wikmd.plugins.load_plugins import PluginLoader 32 | from wikmd.search import Search, Watchdog 33 | from wikmd.utils import pathify, secure_filename 34 | from wikmd.web_dependencies import get_web_deps 35 | 36 | SESSIONS = [] 37 | 38 | cfg = WikmdConfig() 39 | 40 | UPLOAD_FOLDER_PATH = pathify(cfg.wiki_directory, cfg.images_route) 41 | GIT_FOLDER_PATH = pathify(cfg.wiki_directory, '.git') 42 | DRAWING_FOLDER_PATH = pathify(cfg.wiki_directory, cfg.drawings_route) 43 | HIDDEN_FOLDER_PATH_LIST = [pathify(cfg.wiki_directory, hidden_folder) for hidden_folder in cfg.hide_folder_in_wiki] 44 | HOMEPAGE_PATH = pathify(cfg.wiki_directory, cfg.homepage) 45 | HIDDEN_PATHS = tuple([UPLOAD_FOLDER_PATH, GIT_FOLDER_PATH, DRAWING_FOLDER_PATH, HOMEPAGE_PATH] + HIDDEN_FOLDER_PATH_LIST) 46 | 47 | _project_folder = Path(__file__).parent 48 | app = Flask(__name__, 49 | template_folder=_project_folder / "templates", 50 | static_folder=_project_folder / "static") 51 | 52 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER_PATH 53 | app.config['SECRET_KEY'] = cfg.secret_key 54 | 55 | # console logger 56 | app.logger.setLevel(logging.INFO) 57 | 58 | # file logger 59 | logger = logging.getLogger('werkzeug') 60 | logger.setLevel(logging.ERROR) 61 | 62 | web_deps = get_web_deps(cfg.local_mode, app.logger) 63 | 64 | # plugins 65 | plugins = PluginLoader(flask_app=app, config=cfg, plugins=cfg.plugins, web_deps=web_deps).get_plugins() 66 | 67 | wrm = WikiRepoManager(flask_app=app) 68 | cache = Cache(cfg.cache_dir) 69 | im = ImageManager(app, cfg) 70 | 71 | SYSTEM_SETTINGS = { 72 | "darktheme": False, 73 | "listsortMTime": False, 74 | "web_deps": web_deps, 75 | "plugins": plugins, 76 | "wiki_title": cfg.wiki_title 77 | } 78 | 79 | def process(content: str, page_name: str): 80 | """ 81 | Function that processes the content with the plugins. 82 | It also manages CRLF to LF conversion. 83 | :param content: content 84 | :param page_name: name of the page 85 | :return processed content 86 | """ 87 | # Convert Win line ending (CRLF) to standard Unix (LF) 88 | processed = content.replace("\r\n", "\n") 89 | 90 | # Process the content with the plugins 91 | for plugin in plugins: 92 | if "process_md" in dir(plugin): 93 | app.logger.info(f"Plug/{plugin.get_plugin_name()} - process_md >>> {page_name}") 94 | processed = plugin.process_md(processed) 95 | 96 | return processed 97 | 98 | 99 | def ensure_page_can_be_created(page, page_name): 100 | filename = safe_join(cfg.wiki_directory, f"{page_name}.md") 101 | if filename is None: 102 | flash(f"Page name not accepted. Contains disallowed characters.") 103 | app.logger.info(f"Page name isn't secure >>> {page_name}.") 104 | else: 105 | path_exists = os.path.exists(filename) 106 | safe_name = "/".join([secure_filename(part) for part in page_name.split("/")]) 107 | filename_is_ok = safe_name == page_name 108 | if not path_exists and filename_is_ok and page_name: # Early exist 109 | return 110 | 111 | if path_exists: 112 | flash('A page with that name already exists. The page name needs to be unique.') 113 | app.logger.info(f"Page name exists >>> {page_name}.") 114 | 115 | if not filename_is_ok: 116 | flash(f"Page name not accepted. Try using '{safe_name}'.") 117 | app.logger.info(f"Page name isn't secure >>> {page_name}.") 118 | 119 | if not page_name: 120 | flash(f"Your page needs a name.") 121 | app.logger.info(f"No page name provided.") 122 | 123 | content = process(request.form['CT'], page_name) 124 | return render_template( 125 | "new.html", 126 | content=content, 127 | title=page, 128 | upload_path=cfg.images_route, 129 | image_allowed_mime=cfg.image_allowed_mime, 130 | system=SYSTEM_SETTINGS 131 | ) 132 | 133 | 134 | def save(page_name): 135 | """ 136 | Function that processes and saves a *.md page. 137 | :param page_name: name of the page 138 | """ 139 | content = process(request.form['CT'], page_name) 140 | app.logger.info(f"Saving >>> '{page_name}' ...") 141 | 142 | try: 143 | filename = safe_join(cfg.wiki_directory, f"{page_name}.md") 144 | dirname = os.path.dirname(filename) 145 | if not os.path.exists(dirname): 146 | os.makedirs(dirname) 147 | with open(filename, 'w', encoding="utf-8") as f: 148 | f.write(content) 149 | except Exception as e: 150 | app.logger.error(f"Error while saving '{page_name}' >>> {str(e)}") 151 | 152 | 153 | def search(search_term: str, page: int): 154 | """ 155 | Function that searches for a term and shows the results. 156 | """ 157 | app.logger.info(f"Searching >>> '{search_term}' ...") 158 | search = Search(cfg.search_dir) 159 | page = int(page) 160 | results, num_results, num_pages, suggestions = search.search(search_term, page) 161 | return render_template( 162 | 'search.html', 163 | search_term=search_term, 164 | num_results=num_results, 165 | num_pages=num_pages, 166 | current_page=page, 167 | suggestions=suggestions, 168 | results=results, 169 | system=SYSTEM_SETTINGS 170 | ) 171 | 172 | 173 | def fetch_page_name() -> str: 174 | page_name = request.form['PN'] 175 | if page_name[-4:] == "{id}": 176 | page_name = f"{page_name[:-4]}{uuid.uuid4().hex}" 177 | return page_name 178 | 179 | 180 | def get_html(file_page): 181 | """ 182 | Function to return the html of a certain file page 183 | """ 184 | md_file_path = safe_join(cfg.wiki_directory, f"{file_page}.md") 185 | mod = "Last modified: %s" % time.ctime(os.path.getmtime(md_file_path)) 186 | folder = file_page.split("/") 187 | file_page = folder[-1:][0] 188 | folder = folder[:-1] 189 | folder = "/".join(folder) 190 | 191 | cached_entry = cache.get(md_file_path) 192 | if cached_entry: 193 | app.logger.info(f"Showing HTML page from cache >>> '{file_page}'") 194 | 195 | for plugin in plugins: 196 | if "process_html" in dir(plugin): 197 | app.logger.info(f"Plug/{plugin.get_plugin_name()} - process_html >>> {file_page}") 198 | cached_entry = plugin.process_html(cached_entry) 199 | 200 | return cached_entry, mod 201 | 202 | with open(md_file_path, 'r', encoding="utf-8") as file: 203 | md_file_content = file.read() 204 | 205 | for plugin in plugins: 206 | if "process_md_before_html_convert" in dir(plugin): 207 | app.logger.info(f"Plug/{plugin.get_plugin_name()} - process_md_before_html_convert >>> {file_page}") 208 | md_file_content = plugin.process_md_before_html_convert(md_file_content) 209 | 210 | app.logger.info(f"Converting to HTML with pandoc >>> '{md_file_path}' ...") 211 | html = pypandoc.convert_text(md_file_content, "html5", 212 | format='md', extra_args=["--mathjax"], filters=['pandoc-xnos']) 213 | 214 | if html.strip(): 215 | html = clean_html(html) 216 | 217 | for plugin in plugins: 218 | if "process_before_cache_html" in dir(plugin): 219 | app.logger.info(f"Plug/{plugin.get_plugin_name()} - process_before_cache_html >>> {file_page}") 220 | html = plugin.process_before_cache_html(html) 221 | 222 | cache.set(md_file_path, html) 223 | 224 | for plugin in plugins: 225 | if "process_html" in dir(plugin): 226 | app.logger.info(f"Plug/{plugin.get_plugin_name()} - process_html >>> {file_page}") 227 | html = plugin.process_html(html) 228 | 229 | app.logger.info(f"Showing HTML page >>> '{file_page}'") 230 | 231 | return html, mod 232 | 233 | 234 | @app.route('/list/', methods=['GET']) 235 | def list_full_wiki(): 236 | return list_wiki("") 237 | 238 | 239 | @app.route('/list//', methods=['GET']) 240 | def list_wiki(folderpath): 241 | """ 242 | Lists all the pages in a given folder of the wiki. 243 | """ 244 | files_list = [] 245 | 246 | requested_path = safe_join(cfg.wiki_directory, folderpath) 247 | if requested_path is None: 248 | app.logger.info("Requested unsafe path >>> showing homepage") 249 | return index() 250 | app.logger.info(f"Showing >>> all files in {folderpath}") 251 | 252 | for item in os.listdir(requested_path): 253 | item_path = pathify(requested_path, item) # wiki/dir1/dir2/... 254 | item_mtime = os.path.getmtime(item_path) 255 | 256 | if not item_path.startswith(HIDDEN_PATHS): # skip hidden paths 257 | rel_item_path = item_path[len(cfg.wiki_directory + "/"):] # dir1/dir2/... 258 | item_url = os.path.splitext(rel_item_path)[0] # eventually drop the extension 259 | folder = rel_item_path if os.path.isdir(item_path) else "" 260 | 261 | info = { 262 | 'doc': item, 263 | 'url': item_url, 264 | 'folder': folder, 265 | 'folder_url': folder, 266 | 'mtime': item_mtime, 267 | } 268 | files_list.append(info) 269 | 270 | # Sorting 271 | if SYSTEM_SETTINGS['listsortMTime']: 272 | files_list.sort(key=lambda x: x["mtime"], reverse=True) 273 | else: 274 | files_list.sort(key=lambda x: (str(x["url"]).casefold())) 275 | 276 | return render_template( 277 | 'list_files.html', 278 | list=files_list, 279 | folder=folderpath, 280 | system=SYSTEM_SETTINGS 281 | ) 282 | 283 | 284 | @app.route('/search', methods=['GET']) 285 | def search_route(): 286 | if request.args.get("q"): 287 | return search(request.args.get("q"), request.args.get("page", 1)) 288 | flash("You didn't enter anything to search for") 289 | return redirect("/") 290 | 291 | 292 | @app.route('/', methods=['GET']) 293 | def file_page(file_page): 294 | git_sync_thread = Thread(target=wrm.git_pull, args=()) 295 | git_sync_thread.start() 296 | 297 | if "favicon" in file_page: # if the GET request is not for the favicon 298 | return 299 | 300 | try: 301 | html_content, mod = get_html(file_page) 302 | 303 | return render_template( 304 | 'content.html', 305 | title=file_page, 306 | folder="", 307 | info=html_content, 308 | modif=mod, 309 | system=SYSTEM_SETTINGS 310 | ) 311 | except FileNotFoundError as e: 312 | app.logger.info(e) 313 | return redirect("/add_new?page=" + file_page) 314 | 315 | 316 | @app.route('/', methods=['GET']) 317 | def index(): 318 | html = "" 319 | app.logger.info("Showing HTML page >>> 'homepage'") 320 | 321 | try: 322 | if cfg.homepage.lower().endswith(".md"): 323 | html, mod = get_html(cfg.homepage[:-len(".md")]) 324 | except Exception as e: 325 | app.logger.error(f"Conversion to HTML failed >>> {str(e)}") 326 | 327 | return render_template('index.html', homepage=html, system=SYSTEM_SETTINGS) 328 | 329 | 330 | @app.route('/add_new', methods=['POST', 'GET']) 331 | def add_new(): 332 | if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): 333 | return login("/add_new") 334 | if request.method == 'POST': 335 | page_name = fetch_page_name() 336 | 337 | re_render_page = ensure_page_can_be_created(page_name, page_name) 338 | if re_render_page: 339 | return re_render_page 340 | 341 | save(page_name) 342 | git_sync_thread = Thread(target=wrm.git_sync, args=(page_name, "Add")) 343 | git_sync_thread.start() 344 | 345 | return redirect(url_for("file_page", file_page=page_name)) 346 | else: 347 | page_name = request.args.get("page") 348 | if page_name is None: 349 | page_name = "" 350 | return render_template( 351 | 'new.html', 352 | upload_path=cfg.images_route, 353 | image_allowed_mime=cfg.image_allowed_mime, 354 | title=page_name, 355 | system=SYSTEM_SETTINGS 356 | ) 357 | 358 | 359 | @app.route('/edit/homepage', methods=['POST', 'GET']) 360 | def edit_homepage(): 361 | if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): 362 | return login("edit/homepage") 363 | 364 | if request.method == 'POST': 365 | page_name = fetch_page_name() 366 | 367 | save(page_name) 368 | git_sync_thread = Thread(target=wrm.git_sync, args=(page_name, "Edit")) 369 | git_sync_thread.start() 370 | 371 | return redirect(url_for("file_page", file_page=page_name)) 372 | else: 373 | 374 | with open(os.path.join(cfg.wiki_directory, cfg.homepage), 'r', encoding="utf-8", errors='ignore') as f: 375 | 376 | content = f.read() 377 | return render_template( 378 | "new.html", 379 | content=content, 380 | title=cfg.homepage_title, 381 | upload_path=cfg.images_route, 382 | image_allowed_mime=cfg.image_allowed_mime, 383 | system=SYSTEM_SETTINGS 384 | ) 385 | 386 | 387 | @app.route('/remove/', methods=['GET']) 388 | def remove(page): 389 | if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): 390 | return redirect(url_for("file_page", file_page=page)) 391 | 392 | filename = safe_join(cfg.wiki_directory, f"{page}.md") 393 | os.remove(filename) 394 | if not os.listdir(os.path.dirname(filename)): 395 | os.removedirs(os.path.dirname(filename)) 396 | git_sync_thread = Thread(target=wrm.git_sync, args=(page, "Remove")) 397 | git_sync_thread.start() 398 | return redirect("/") 399 | 400 | 401 | @app.route('/edit/', methods=['POST', 'GET']) 402 | def edit(page): 403 | if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): 404 | return login("edit/" + page) 405 | 406 | filename = safe_join(cfg.wiki_directory, f"{page}.md") 407 | if request.method == 'POST': 408 | page_name = fetch_page_name() 409 | 410 | if page_name != page: 411 | re_render_page = ensure_page_can_be_created(page_name, page_name) 412 | if re_render_page: 413 | return re_render_page 414 | 415 | os.remove(filename) 416 | 417 | save(page_name) 418 | git_sync_thread = Thread(target=wrm.git_sync, args=(page_name, "Edit")) 419 | git_sync_thread.start() 420 | 421 | return redirect(url_for("file_page", file_page=page_name)) 422 | else: 423 | if exists(filename): 424 | with open(filename, 'r', encoding="utf-8", errors='ignore') as f: 425 | content = f.read() 426 | return render_template( 427 | "new.html", 428 | content=content, 429 | title=page, 430 | upload_path=cfg.images_route, 431 | image_allowed_mime=cfg.image_allowed_mime, 432 | system=SYSTEM_SETTINGS 433 | ) 434 | else: 435 | logger.error(f"{filename} does not exists. Creating a new one.") 436 | return render_template( 437 | "new.html", 438 | content="", 439 | title=page, 440 | upload_path=cfg.images_route, 441 | image_allowed_mime=cfg.image_allowed_mime, 442 | system=SYSTEM_SETTINGS 443 | ) 444 | 445 | 446 | @app.route(os.path.join("/", cfg.images_route), methods=['POST', 'DELETE']) 447 | def upload_file(): 448 | if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): 449 | return login() 450 | app.logger.info(f"Uploading new image ...") 451 | # Upload image when POST 452 | if request.method == "POST": 453 | return im.save_images(request.files) 454 | 455 | # DELETE when DELETE 456 | if request.method == "DELETE": 457 | # request data is in format "b'nameoffile.png" decode to utf-8 458 | file_name = request.data.decode("utf-8") 459 | im.delete_image(file_name) 460 | return 'OK' 461 | 462 | 463 | @app.route("/plug_com", methods=['POST']) 464 | def communicate_plugins(): 465 | if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): 466 | return login() 467 | if request.method == "POST": 468 | for plugin in plugins: 469 | if "communicate_plugin" in dir(plugin): 470 | return plugin.communicate_plugin(request) 471 | return "nothing to do" 472 | 473 | 474 | @app.route('/knowledge-graph', methods=['GET']) 475 | def graph(): 476 | global links 477 | links = knowledge_graph.find_links() 478 | return render_template("knowledge-graph.html", links=links, system=SYSTEM_SETTINGS) 479 | 480 | 481 | @app.route('/login', methods=['GET', 'POST']) 482 | def login(page): 483 | if request.method == "POST": 484 | password = request.form["password"] 485 | sha_string = sha256(password.encode('utf-8')).hexdigest() 486 | if sha_string == cfg.password_in_sha_256.lower(): 487 | app.logger.info("User successfully logged in") 488 | resp = make_response(redirect("/" + page)) 489 | session = secrets.token_urlsafe(1024 // 8) 490 | resp.set_cookie("session_wikmd", session) 491 | SESSIONS.append(session) 492 | return resp 493 | else: 494 | app.logger.info("Login failed!") 495 | else: 496 | app.logger.info("Display login page") 497 | return render_template("login.html", system=SYSTEM_SETTINGS) 498 | 499 | 500 | # Translate id to page path 501 | 502 | 503 | @app.route('/nav//', methods=['GET']) 504 | def nav_id_to_page(id): 505 | for i in links: 506 | if i["id"] == int(id): 507 | return redirect("/" + i["path"]) 508 | return redirect("/") 509 | 510 | 511 | @app.route(f"/{cfg.images_route}/") 512 | def display_image(image_name): 513 | image_path = safe_join(UPLOAD_FOLDER_PATH, image_name) 514 | try: 515 | response = send_file(Path(image_path).resolve()) 516 | except Exception: 517 | app.logger.error(f"Could not find image: {image_path}") 518 | return "" 519 | 520 | app.logger.info(f"Showing image >>> '{image_path}'") 521 | # cache indefinitely 522 | response.headers["Cache-Control"] = "max-age=31536000, immutable" 523 | return response 524 | 525 | 526 | @app.route('/toggle-darktheme/', methods=['GET']) 527 | def toggle_darktheme(): 528 | SYSTEM_SETTINGS['darktheme'] = not SYSTEM_SETTINGS['darktheme'] 529 | return redirect(request.args.get("return", "/")) # redirect to the same page URL 530 | 531 | 532 | @app.route('/toggle-sorting/', methods=['GET']) 533 | def toggle_sort(): 534 | SYSTEM_SETTINGS['listsortMTime'] = not SYSTEM_SETTINGS['listsortMTime'] 535 | return redirect("/list") 536 | 537 | 538 | @app.route('/favicon.ico') 539 | def favicon(): 540 | return send_from_directory(os.path.join(app.root_path, 'static'), 541 | 'favicon.ico', mimetype='image/vnd.microsoft.icon') 542 | 543 | 544 | def setup_search(): 545 | search = Search(cfg.search_dir, create=True) 546 | 547 | app.logger.info("Search index creation...") 548 | items = [] 549 | for root, subfolder, files in os.walk(cfg.wiki_directory): 550 | for item in files: 551 | if ( 552 | root.startswith(os.path.join(cfg.wiki_directory, '.git')) or 553 | root.startswith(os.path.join(cfg.wiki_directory, cfg.images_route)) 554 | ): 555 | continue 556 | page_name, ext = os.path.splitext(item) 557 | if ext.lower() != ".md": 558 | continue 559 | path = os.path.relpath(root, cfg.wiki_directory) 560 | items.append((item, page_name, path)) 561 | 562 | search.index_all(cfg.wiki_directory, items) 563 | 564 | 565 | def setup_wiki_template() -> bool: 566 | """Copy wiki_template files into the wiki directory if it's empty.""" 567 | root = Path(__file__).parent 568 | 569 | if not os.path.exists(cfg.wiki_directory): 570 | app.logger.info("Wiki directory doesn't exists, copy template") 571 | shutil.copytree(root / "wiki_template", cfg.wiki_directory) 572 | return True 573 | if len(os.listdir(cfg.wiki_directory)) == 0: 574 | app.logger.info("Wiki directory is empty, copy template") 575 | shutil.copytree(root / "wiki_template", cfg.wiki_directory, dirs_exist_ok=True) 576 | return True 577 | 578 | for plugin in plugins: 579 | if "post_setup" in dir(plugin): 580 | plugin.post_setup() 581 | 582 | return False 583 | 584 | 585 | def run_wiki() -> None: 586 | """Run the wiki as a Flask app.""" 587 | app.logger.info("Starting Wikmd with wiki directory %s", Path(cfg.wiki_directory).resolve()) 588 | if int(cfg.wikmd_logging) == 1: 589 | logging.basicConfig(filename=cfg.wikmd_logging_file, level=logging.INFO) 590 | 591 | setup_wiki_template() 592 | 593 | if not os.path.exists(UPLOAD_FOLDER_PATH): 594 | app.logger.info(f"Creating upload folder >>> {UPLOAD_FOLDER_PATH}") 595 | os.mkdir(UPLOAD_FOLDER_PATH) 596 | 597 | wrm.initialize() 598 | im.cleanup_images() 599 | setup_search() 600 | app.logger.info("Spawning search indexer watchdog") 601 | watchdog = Watchdog(cfg.wiki_directory, cfg.search_dir) 602 | watchdog.start() 603 | app.run(host=cfg.wikmd_host, port=cfg.wikmd_port, debug=True, use_reloader=False) 604 | 605 | 606 | for plugin in plugins: 607 | if "request_html" in dir(plugin): 608 | plugin.request_html(get_html) 609 | 610 | if __name__ == '__main__': 611 | run_wiki() 612 | -------------------------------------------------------------------------------- /src/wikmd/wiki_template/Features.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Features 3 | author: Linbreux 4 | --- 5 | 6 | 7 | # Footnotes 8 | ``` 9 | Here is a footnote reference,[^1] and another. 10 | 11 | [^1]: Here is the footnote. 12 | ``` 13 | Here is a footnote reference,[^1] and another. 14 | 15 | [^1]: Here is the footnote. 16 | 17 | ``` 18 | Here is an inline note.^[Inlines notes are easier to write, since 19 | you don't have to pick an identifier and move down to type the 20 | note.] 21 | ``` 22 | 23 | Here is an inline note.^[Inlines notes are easier to write, since 24 | you don't have to pick an identifier and move down to type the 25 | note.] 26 | 27 | ``` 28 | (@good) This is a good example. 29 | 30 | As (@good) illustrates, ... 31 | 32 | ``` 33 | 34 | (@good) This is a good example. 35 | 36 | As (@good) illustrates, ... 37 | 38 | # Split lists 39 | 40 | ``` 41 | 1. one 42 | 2. two 43 | 3. three 44 | 45 | 46 | 47 | 1. uno 48 | 2. dos 49 | 3. tres 50 | ``` 51 | 1. one 52 | 2. two 53 | 3. three 54 | 55 | 56 | 57 | 58 | 1. uno 59 | 2. dos 60 | 3. tres 61 | 62 | 63 | ## Change image size 64 | ``` 65 | ![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} 66 | ``` 67 | ![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} 68 | 69 | ## References 70 | 71 | For references we use [pandoc-xnos](https://github.com/tomduck/pandoc-xnos) 72 | 73 | ### Images 74 | 75 | ``` 76 | ![This is a landscape](https://i.ibb.co/Dzp0SfC/download.jpg){#fig:id width="50%"} 77 | 78 | As show in @fig:id theire is a nice landscape 79 | ``` 80 | ![This is a landscape](https://i.ibb.co/Dzp0SfC/download.jpg){#fig:landscape width="50%"} 81 | 82 | 83 | As show in @fig:landscape theire is a nice landscape 84 | 85 | 86 | ## Math 87 | ``` 88 | $y = mx + b$ {#eq:id} 89 | 90 | This is visible in @eq:id 91 | ``` 92 | $y = mx + b$ {#eq:id} 93 | 94 | This is visible in @eq:id 95 | 96 | ## Etc 97 | 98 | This is also possible for tables and sections. Same princip but with 99 | 100 | ``` 101 | {#tbl:id} (for tables) 102 | {#sec:2} (for sections) 103 | ``` 104 | 105 | # Pandoc 106 | 107 | All default pandoc features are supported with the extend of mathjax and pandoc-xnos. 108 | ![caption](/img/3a2ce07d2109eb82f779f71748be8990.webp) 109 | ![caption](/img/pixil-frame-07165101.png) -------------------------------------------------------------------------------- /src/wikmd/wiki_template/How to use the wiki.md: -------------------------------------------------------------------------------- 1 | ## Homepage 2 | 3 | The homepage is default the `homepage.md` file, this can't be changed. If this file doesn't exist create it in de wiki folder. 4 | 5 | ## Plugins 6 | 7 | The plugins are used to extend the functionality of the wiki. Most of them are accessible through the use of `tags`. 8 | For now there are only a few supported. 9 | 10 | - `[[draw]]` Allows you to add an **interactive drawio drawing** to the wiki. 11 | - `[[info]]`, `[[warning]]`, `[[danger]]`, `[[success]]` Adds a nice **alert message**. 12 | - `[[ page: some-page ]]` Allows to show an other page in the current one. 13 | 14 | [[success]] You are ready to go! 15 | 16 | ## Latex 17 | 18 | It's possible to use latex syntax inside your markdown because the markdown is first converted to latex and after that to html. This means you have a lot more flexibility. 19 | 20 | ### Change image size 21 | ``` 22 | ![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} 23 | ``` 24 | ![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} 25 | 26 | ### Image references 27 | ``` 28 | ![\label{test}](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} 29 | 30 | Inside picture \ref{landscape picture} you can see a nice mountain. 31 | 32 | ``` 33 | ![picture \label{landscape picture}](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} 34 | 35 | Clickable reference in picture \ref{landscape picture}. 36 | 37 | ### Math 38 | ``` 39 | \begin{align} 40 | y(x) &= \int_0^\infty x^{2n} e^{-a x^2}\,dx\\ 41 | &= \frac{2n-1}{2a} \int_0^\infty x^{2(n-1)} e^{-a x^2}\,dx\\ 42 | &= \frac{(2n-1)!!}{2^{n+1}} \sqrt{\frac{\pi}{a^{2n+1}}}\\ 43 | &= \frac{(2n)!}{n! 2^{2n+1}} \sqrt{\frac{\pi}{a^{2n+1}}} 44 | \end{align} 45 | ``` 46 | \begin{align} 47 | y(x) &= \int_0^\infty x^{2n} e^{-a x^2}\,dx\\ 48 | &= \frac{2n-1}{2a} \int_0^\infty x^{2(n-1)} e^{-a x^2}\,dx\\ 49 | &= \frac{(2n-1)!!}{2^{n+1}} \sqrt{\frac{\pi}{a^{2n+1}}}\\ 50 | &= \frac{(2n)!}{n! 2^{2n+1}} \sqrt{\frac{\pi}{a^{2n+1}}} 51 | \end{align} 52 | 53 | ``` 54 | You can also use $inline$ math to show $a=2$ and $b=8$ 55 | ``` 56 | You can also use $inline$ math to show $a=2$ and $b=8$ 57 | 58 | And many other latex functions. 59 | 60 | ## Converting the files 61 | 62 | Open the wiki folder of your instance. 63 | 64 | |- static 65 | |- templates 66 | |- **wiki** $\leftarrow$ This folder 67 | |- wiki.py 68 | 69 | In this folder all the markdownfiles are listed. Editing the files will be visible in the web-version. 70 | 71 | |- homepage.md 72 | |- How to use the wiki.md 73 | |- Markdown cheatsheet.md 74 | 75 | The advantage is that u can use the commandline to process some data. For example using pandoc: 76 | ``` 77 | $ pandoc -f markdown -t latex homepage.md How\ to\ use\ the\ wiki.md -o file.pdf --pdf-engine=xelatex 78 | ``` 79 | This creates a nice pdf version of your article. Its possible you have to create a yml header on top of your document to set the margins etc better 80 | ``` 81 | --- 82 | title: titlepage 83 | author: your name 84 | date: 05-11-2020 85 | geometry: margin=2.5cm 86 | header-includes: | 87 | \usepackage{caption} 88 | \usepackage{subcaption} 89 | lof: true 90 | --- 91 | ``` 92 | For more information you have to read the pandoc documentation. 93 | 94 | [Using the version control system](/Using the version control system) 95 | -------------------------------------------------------------------------------- /src/wikmd/wiki_template/Markdown cheatsheet.md: -------------------------------------------------------------------------------- 1 | # Heading 1 # 2 | 3 | Markup : # Heading 1 # 4 | 5 | -OR- 6 | 7 | Markup : ============= (below H1 text) 8 | 9 | ## Heading 2 ## 10 | 11 | Markup : ## Heading 2 ## 12 | 13 | -OR- 14 | 15 | Markup: --------------- (below H2 text) 16 | 17 | ### Heading 3 ### 18 | 19 | Markup : ### Heading 3 ### 20 | 21 | #### Heading 4 #### 22 | 23 | Markup : #### Heading 4 #### 24 | 25 | 26 | Common text 27 | 28 | Markup : Common text 29 | 30 | _Emphasized text_ 31 | 32 | Markup : _Emphasized text_ or *Emphasized text* 33 | 34 | ~~Strikethrough text~~ 35 | 36 | Markup : ~~Strikethrough text~~ 37 | 38 | __Strong text__ 39 | 40 | Markup : __Strong text__ or **Strong text** 41 | 42 | ___Strong emphasized text___ 43 | 44 | Markup : ___Strong emphasized text___ or ***Strong emphasized text*** 45 | 46 | [Named Link](http://www.google.fr/ "Named link title") and http://www.google.fr/ or 47 | 48 | Markup : [Named Link](http://www.google.fr/ "Named link title") and http://www.google.fr/ or 49 | 50 | [heading-1](#heading-1 "Goto heading-1") 51 | 52 | Markup: [heading-1](#heading-1 "Goto heading-1") 53 | 54 | Table, like this one : 55 | 56 | First Header | Second Header 57 | ------------- | ------------- 58 | Content Cell | Content Cell 59 | Content Cell | Content Cell 60 | 61 | ``` 62 | First Header | Second Header 63 | ------------- | ------------- 64 | Content Cell | Content Cell 65 | Content Cell | Content Cell 66 | ``` 67 | 68 | Adding a pipe `|` in a cell : 69 | 70 | First Header | Second Header 71 | ------------- | ------------- 72 | Content Cell | Content Cell 73 | Content Cell | \| 74 | 75 | ``` 76 | First Header | Second Header 77 | ------------- | ------------- 78 | Content Cell | Content Cell 79 | Content Cell | \| 80 | ``` 81 | 82 | Left, right and center aligned table 83 | 84 | Left aligned Header | Right aligned Header | Center aligned Header 85 | | :--- | ---: | :---: 86 | Content Cell | Content Cell | Content Cell 87 | Content Cell | Content Cell | Content Cell 88 | 89 | ``` 90 | Left aligned Header | Right aligned Header | Center aligned Header 91 | | :--- | ---: | :---: 92 | Content Cell | Content Cell | Content Cell 93 | Content Cell | Content Cell | Content Cell 94 | ``` 95 | 96 | `code()` 97 | 98 | Markup : `code()` 99 | 100 | ```javascript 101 | var specificLanguage_code = 102 | { 103 | "data": { 104 | "lookedUpPlatform": 1, 105 | "query": "Kasabian+Test+Transmission", 106 | "lookedUpItem": { 107 | "name": "Test Transmission", 108 | "artist": "Kasabian", 109 | "album": "Kasabian", 110 | "picture": null, 111 | "link": "http://open.spotify.com/track/5jhJur5n4fasblLSCOcrTp" 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | Markup : ```javascript 118 | ``` 119 | 120 | * Bullet list 121 | * Nested bullet 122 | * Sub-nested bullet etc 123 | * Bullet list item 2 124 | 125 | ~~~ 126 | Markup : * Bullet list 127 | * Nested bullet 128 | * Sub-nested bullet etc 129 | * Bullet list item 2 130 | 131 | -OR- 132 | 133 | Markup : - Bullet list 134 | - Nested bullet 135 | - Sub-nested bullet etc 136 | - Bullet list item 2 137 | ~~~ 138 | 139 | 1. A numbered list 140 | 1. A nested numbered list 141 | 2. Which is numbered 142 | 2. Which is numbered 143 | 144 | ~~~ 145 | Markup : 1. A numbered list 146 | 1. A nested numbered list 147 | 2. Which is numbered 148 | 2. Which is numbered 149 | ~~~ 150 | 151 | - [ ] An uncompleted task 152 | - [x] A completed task 153 | 154 | ~~~ 155 | Markup : - [ ] An uncompleted task 156 | - [x] A completed task 157 | ~~~ 158 | 159 | - [ ] An uncompleted task 160 | - [ ] A subtask 161 | 162 | ~~~ 163 | Markup : - [ ] An uncompleted task 164 | - [ ] A subtask 165 | ~~~ 166 | 167 | > Blockquote 168 | >> Nested blockquote 169 | 170 | Markup : > Blockquote 171 | >> Nested Blockquote 172 | 173 | _Horizontal line :_ 174 | - - - - 175 | 176 | Markup : - - - - 177 | 178 | _Image with alt :_ 179 | 180 | ![picture alt](http://via.placeholder.com/200x150 "Title is optional") 181 | 182 | Markup : ![picture alt](http://via.placeholder.com/200x150 "Title is optional") 183 | 184 | Foldable text: 185 | 186 |
      187 | Title 1 188 |

      Content 1 Content 1 Content 1 Content 1 Content 1

      189 |
      190 |
      191 | Title 2 192 |

      Content 2 Content 2 Content 2 Content 2 Content 2

      193 |
      194 | 195 | Markup :
      196 | Title 1 197 |

      Content 1 Content 1 Content 1 Content 1 Content 1

      198 |
      199 | 200 | ```html 201 |

      HTML

      202 |

      Some HTML code here

      203 | ``` 204 | 205 | Link to a specific part of the page: 206 | 207 | [Go To TOP](#TOP) 208 | 209 | Markup : [text goes here](#section_name) 210 | section_title 211 | 212 | Hotkey: 213 | 214 | ⌘F 215 | 216 | ⇧⌘F 217 | 218 | Markup : ⌘F 219 | 220 | Hotkey list: 221 | 222 | | Key | Symbol | 223 | | --- | --- | 224 | | Option | ⌥ | 225 | | Control | ⌃ | 226 | | Command | ⌘ | 227 | | Shift | ⇧ | 228 | | Caps Lock | ⇪ | 229 | | Tab | ⇥ | 230 | | Esc | ⎋ | 231 | | Power | ⌽ | 232 | | Return | ↩ | 233 | | Delete | ⌫ | 234 | | Up | ↑ | 235 | | Down | ↓ | 236 | | Left | ← | 237 | | Right | → | 238 | 239 | Emoji: 240 | 241 | :exclamation: Use emoji icons to enhance text. :+1: Look up emoji codes at [emoji-cheat-sheet.com](http://emoji-cheat-sheet.com/) 242 | 243 | Markup : Code appears between colons :EMOJICODE: -------------------------------------------------------------------------------- /src/wikmd/wiki_template/Using the version control system.md: -------------------------------------------------------------------------------- 1 | ## Git 2 | 3 | We use git as a version control system. Everytime you save a file it will commit it to git. You could also use the cli to add and commit files, make sure you are in the "wiki" folder, if you are still in the "wikmd" folder you are using the wrong git folder. 4 | 5 | ``` 6 | git add . (or the specific file) 7 | git commit -m "your message" (default date of today) 8 | ``` 9 | 10 | or you could just go to the homepage of the wiki, this will do all these automatic. 11 | 12 | ## How to go to previous file? 13 | 14 | cd inside 'wikmd/wiki' 15 | 16 | Find the version you would like to revert to. 17 | 18 | ``` 19 | git log -p file.md 20 | ``` 21 | 22 | This will give you a long commit string. Copy the first part of it. (for example b4b580411b) 23 | 24 | Modify the file 25 | 26 | ``` 27 | git checkout b4b580411b -- file.md 28 | ``` 29 | 30 | Now reload the homepage or use [git](#git) 31 | -------------------------------------------------------------------------------- /src/wikmd/wiki_template/homepage.md: -------------------------------------------------------------------------------- 1 | ## What is it? 2 | It’s a file-based wiki that aims to simplicity. The documents are completely written in Markdown which is an easy markup language that you can learn in 60 sec. 3 | 4 | ## Why markdown? 5 | If you compare markdown to a WYSIWYG editor it may look less easy to use but the opposite is true. When writing markdown you don’t get to see the result directly which is the only downside. 6 | There are more pros: 7 | 8 | - Easy to process to other file formats 9 | - Scalable, it reformats for the perfect display width 10 | 11 | ## How does it work? 12 | Instead of storing the data in a database I chose to have a file-based system. The advantage of this system is that every file is directly readable inside a terminal etc. Also when you have direct access to the system you can export the files to anything you like. 13 | 14 | To view the documents in the browser, the document is converted to html. 15 | 16 | ## Plugins (beta) 17 | 18 | The plugins are used to extend the functionality of the wiki. Most of them are accessible through the use of `tags`. 19 | For now there are only a few supported. 20 | 21 | - `[[draw]]` Allows you to add an **interactive drawio drawing** to the wiki. 22 | - `[[info]]`, `[[warning]]`, `[[danger]]`, `[[success]]` Adds a nice **alert message**. 23 | - `[[ page: some-page ]]` Allows to show an other page in the current one. 24 | - `[[swagger link]]` Allows to insert a **swagger** block into the wiki page. Link in annotation should lead 25 | to a GET endpoint with .json openapi file. `[[swagger https://petstore3.swagger.io/api/v3/openapi.json]]` 26 | can be used as an example. 27 | - \`\`\`plantuml CODE \`\`\` Allows to embed a [plantuml](https://plantuml.com) diagram. 28 | A custom plantuml server can be defined using configuration file. 29 | - \`\`\`mermaid CODE \`\`\` Allows to embed a [mermaid](https://mermaid.js.org/intro/) diagram. 30 | 31 | You can read more about plugins [here](https://linbreux.github.io/wikmd/plugins.html). 32 | 33 | 34 | ### Image support 35 | ![](https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Markdown-mark.svg/208px-Markdown-mark.svg.png) 36 | 37 | ### Latex support 38 | 39 | $$x_{1,2} = \frac{-b ± \sqrt{b^2 - 4 a c}}{2a}$$ 40 | 41 | ## How to use the wiki 42 | You can learn more on how to use the wiki [here](How to use the wiki) 43 | 44 | ## Features 45 | Read all the features [here](Features) 46 | 47 | ## Handy links 48 | - [Google](http://google.be) 49 | - [Duckduckgo](http://duckduckgo.org) 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/wikmd/wikmd-config.yaml: -------------------------------------------------------------------------------- 1 | # wikmd configuration file 2 | 3 | wikmd_host: "0.0.0.0" 4 | wikmd_port: 5000 5 | wikmd_logging: 1 6 | wikmd_logging_file: "wikmd.log" 7 | 8 | git_user: "wikmd" 9 | git_email: "wikmd@no-mail.com" 10 | 11 | main_branch_name: "main" 12 | sync_with_remote: 0 13 | remote_url: "" 14 | 15 | wiki_directory: "wiki" 16 | wiki_title: "Wiki" 17 | homepage: "homepage.md" 18 | homepage_title: "homepage" 19 | images_route: "img" 20 | image_allowed_mime: ["image/gif", "image/jpeg", "image/png", "image/svg+xml", "image/webp"] 21 | hide_folder_in_wiki: [".obsidian"] 22 | 23 | plugins: ["draw", "alerts", "mermaid", "embed-pages", "swagger", "plantuml"] 24 | plantuml_server_url: "https://www.plantuml.com/plantuml" 25 | 26 | protect_edit_by_password: 0 27 | password_in_sha_256: "0E9C700FAB2D5B03B0581D080E74A2D7428758FC82BD423824C6C11D6A7F155E" #ps: wikmd 28 | 29 | local_mode: false 30 | 31 | optimize_images: "no" 32 | 33 | cache_dir: "/dev/shm/wikmd/cache" 34 | search_dir: "/dev/shm/wikmd/searchindex" 35 | 36 | secret_key: '\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O:"/\|?*' 115 | 116 | r = app.test_client().post("/add_new", data={ 117 | "PN": bad_all_bad, 118 | "CT": "#testing file\n this is a test", 119 | }) 120 | assert r.status_code == 200 121 | assert b"Page name not accepted." in r.data 122 | 123 | r = app.test_client().post("/add_new", data={ 124 | "PN": bad_name, 125 | "CT": "#testing file\n this is a test", 126 | }) 127 | assert r.status_code == 200 128 | assert bad_name.replace("*", "").encode() in r.data 129 | 130 | 131 | def test_ok_file_names(wiki_path): 132 | """Test for creating files with odd character in names.""" 133 | # Disallowed 134 | ok_name1 = "file with space" 135 | ok_name2 = 'file with slash/is a folder' 136 | r = app.test_client().post("/add_new", data={ 137 | "PN": ok_name1, 138 | "CT": "#testing file\n this is a test", 139 | }) 140 | assert r.status_code == 302 141 | assert (wiki_path / ok_name1).with_suffix(".md").exists() 142 | 143 | r = app.test_client().post("/add_new", data={ 144 | "PN": ok_name2, 145 | "CT": "#testing file\n this is a test", 146 | }) 147 | assert r.status_code == 302 148 | assert (wiki_path / ok_name2).with_suffix(".md").exists() 149 | 150 | 151 | # create a new file in a folder using the wiki and check if it is visible in the wiki 152 | def test_new_file_folder(wiki_path): 153 | """App can create folders.""" 154 | rv = app.test_client().get("/add_new") 155 | assert rv.status_code == 200 156 | assert b"content" in rv.data 157 | 158 | # create new file in a folder 159 | app.test_client().post("/add_new", data={ 160 | "PN": "testingfolder01234/testing01234filenotexisting", 161 | "CT": "#testing file\n this is a test", 162 | }) 163 | 164 | # look at file 165 | rv = app.test_client().get("/testingfolder01234/testing01234filenotexisting") 166 | assert b"testing file" in rv.data 167 | assert b"this is a test" in rv.data 168 | 169 | f = wiki_path / "testingfolder01234" 170 | shutil.rmtree(f) 171 | 172 | 173 | # edits file using the wiki and check if it is visible in the wiki 174 | def test_get_file_after_file_edit(project_file, wiki_file): 175 | with project_file.open("w+") as fp: 176 | fp.write("our new content") 177 | 178 | rv = app.test_client().get(wiki_file) 179 | assert rv.status_code == 200 180 | assert b"our new content" in rv.data 181 | 182 | 183 | def test_get_file_after_api_edit(wiki_file): 184 | # Edit the file through API 185 | app.test_client().post(f"/edit{wiki_file}", data={ 186 | "PN": wiki_file[1:], 187 | "CT": "#testing file\n this is a test", 188 | }) 189 | 190 | rv = app.test_client().get(wiki_file) 191 | assert b"testing file" in rv.data 192 | assert b"this is a test" in rv.data 193 | 194 | 195 | # edits file in folder using the wiki and check if it is visible in the wiki 196 | def test_get_edit_page_content(project_file, wiki_file): 197 | with project_file.open("w+") as fp: 198 | fp.write("# this is the header\n extra content") 199 | 200 | rv = app.test_client().get(f"/edit{wiki_file}") 201 | assert rv.status_code == 200 202 | assert b"this is the header" in rv.data 203 | -------------------------------------------------------------------------------- /tests/test_plugins.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from wikmd import wiki 3 | from wikmd.wiki import app 4 | 5 | 6 | @pytest.fixture() 7 | def client(): 8 | return app.test_client 9 | 10 | 11 | def test_plugin_loading(): 12 | assert wiki.plugins 13 | 14 | 15 | def test_process_md(): 16 | before = "#test this is test\n text should still be available after plugin" 17 | md = before 18 | for plugin in wiki.plugins: 19 | if "process_md" in dir(plugin): 20 | md = plugin.process_md(md) 21 | assert md == before 22 | 23 | 24 | def test_draw_md(): 25 | before = "#test this is test\n[[draw]] \n next line" 26 | md = before 27 | for plugin in wiki.plugins: 28 | if "process_md" in dir(plugin): 29 | md = plugin.process_md(md) 30 | assert md != before 31 | assert md != "" 32 | 33 | 34 | def test_process_html(): 35 | before = "

      this is a test

      " 36 | html = before 37 | for plugin in wiki.plugins: 38 | if "process_html" in dir(plugin): 39 | html = plugin.process_html(html) 40 | assert html == before -------------------------------------------------------------------------------- /tests/test_search.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import time 4 | from pathlib import Path 5 | import pytest 6 | 7 | from wikmd.search import Search, Watchdog 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def search_dir(): 12 | return tempfile.mkdtemp() 13 | 14 | 15 | @pytest.fixture() 16 | def search_file(): 17 | file_name = "test_index.md" 18 | title = "test index" 19 | content = "index\nsearch\ntest" 20 | return file_name, title, content 21 | 22 | 23 | @pytest.fixture() 24 | def search_engine(search_dir): 25 | return Search(search_dir, create=True) 26 | 27 | 28 | @pytest.fixture() 29 | def search_engine_with_content(search_engine, search_dir, search_file): 30 | file_name, title, content = search_file 31 | 32 | search_engine.index(search_dir, file_name, title, content) 33 | return search_engine 34 | 35 | 36 | def test_textify(): 37 | tmp = tempfile.mkdtemp() 38 | s = Search(tmp, create=True) 39 | md = "# h1\n## h2\ntest" 40 | assert s.textify(md) == "h1\nh2\ntest" 41 | 42 | 43 | def test_search(search_engine_with_content, search_dir, search_file): 44 | res, total, pages, _ = search_engine_with_content.search("index", 1) 45 | assert total == 1 46 | assert pages == 1 47 | assert res[0].path == search_dir 48 | assert res[0].filename == search_file[0] 49 | 50 | _, _, _, sug = search_engine_with_content.search("ndex", 1) 51 | assert "index" in sug 52 | 53 | 54 | def test_pagination(search_engine, search_dir): 55 | for i in range(25): 56 | fname, title = f"test_index_{i}.md", f"test index {i}" 57 | content = "index\nsearch\ntest" 58 | search_engine.index(search_dir, fname, title, content) 59 | 60 | res, total, pages, _ = search_engine.search("index", 1) 61 | assert total == 25 62 | assert pages == 3 63 | 64 | 65 | def test_index_and_delete(search_engine_with_content, search_dir, search_file ): 66 | search_engine_with_content.delete(search_dir, search_file[0]) 67 | res, total, pages, _ = search_engine_with_content.search("index", 1) 68 | assert total == 0 69 | assert pages == 0 70 | assert len(res) == 0 71 | 72 | 73 | def test_index_all(search_engine, search_dir): 74 | nf = [] 75 | content = "index\nsearch\ntest" 76 | for n in ("a", "b"): 77 | file_name = f"{n}.md" 78 | p = Path(search_dir) / file_name 79 | with p.open("w") as f: 80 | f.write(content) 81 | nf.append((file_name, n, ".")) 82 | 83 | # Add a file to a sub folder 84 | p_dir = Path(search_dir) 85 | (p_dir / "z").mkdir() 86 | file_name = p_dir / "z" / "y.md" 87 | with file_name.open("w") as f: 88 | f.write(content) 89 | nf.append(("y.md", "y", "z")) 90 | 91 | search_engine.index_all(search_dir, nf) 92 | res, total, pages, _ = search_engine.search("index", 1) 93 | assert total == 3 94 | assert pages == 1 95 | assert len(res) == 3 96 | a, b, z = res 97 | assert a.path == "." 98 | assert a.filename == "a.md" 99 | assert b.path == "." 100 | assert b.filename == "b.md" 101 | assert z.path == "z" 102 | assert z.filename == "y.md" 103 | 104 | 105 | def test_watchdog(): 106 | tmps, tmpd = tempfile.mkdtemp(), tempfile.mkdtemp() 107 | s = Search(tmps, create=True) 108 | w = Watchdog(tmpd, tmps) 109 | w.start() 110 | 111 | assert s.search("index", 1) == ([], 0, 0, []) 112 | 113 | # test index 114 | content = "\n".join(("index", "search" "test")) 115 | fpath = os.path.join(tmpd, "a.md") 116 | with open(fpath, "w") as f: 117 | f.write(content) 118 | 119 | time.sleep(2) 120 | res, total, pages, _ = s.search("index", 1) 121 | assert total == 1 122 | assert pages == 1 123 | assert len(res) == 1 124 | assert res[0].path == "." 125 | assert "index" in res[0].highlights 126 | 127 | # test update 128 | with open(fpath, "w") as f: 129 | content2 = "\n".join(("something", "else", "entirely")) 130 | f.write(content2) 131 | 132 | time.sleep(2) 133 | res, total, pages, _ = s.search("index", 1) 134 | assert total == 0 135 | assert pages == 0 136 | assert len(res) == 0 137 | 138 | res, total, pages, _ = s.search("something", 1) 139 | assert total == 1 140 | assert pages == 1 141 | assert len(res) == 1 142 | assert res[0].path == "." 143 | assert "something" in res[0].highlights 144 | 145 | # test move 146 | os.rename(os.path.join(tmpd, "a.md"), os.path.join(tmpd, "b.md")) 147 | time.sleep(2) 148 | res, total, pages, _ = s.search("something", 1) 149 | assert total == 1 150 | assert pages == 1 151 | assert len(res) == 1 152 | assert res[0].path == "." 153 | assert res[0].filename == "b.md" 154 | assert "something" in res[0].highlights 155 | 156 | # test remove 157 | os.remove(os.path.join(tmpd, "b.md")) 158 | time.sleep(2) 159 | res, total, pages, _ = s.search("index", 1) 160 | assert total == 0 161 | assert pages == 0 162 | assert len(res) == 0 163 | 164 | res, total, pages, _ = s.search("something", 1) 165 | assert total == 0 166 | assert pages == 0 167 | assert len(res) == 0 168 | 169 | 170 | def test_watchdog_subdirectory(): 171 | tmps, tmpd = tempfile.mkdtemp(), tempfile.mkdtemp() 172 | s = Search(tmps, create=True) 173 | w = Watchdog(tmpd, tmps) 174 | w.start() 175 | 176 | assert s.search("index", 1) == ([], 0, 0, []) 177 | # test index subdir 178 | sub_dir = os.path.join(tmpd, "subdir") 179 | os.makedirs(sub_dir) 180 | fpath = os.path.join(sub_dir, "t.md") 181 | with open(fpath, "w") as f: 182 | content2 = "\n".join(("something", "else", "entirely")) 183 | f.write(content2) 184 | 185 | time.sleep(2) 186 | res, total, pages, _ = s.search("something", 1) 187 | assert total == 1 188 | assert pages == 1 189 | assert len(res) == 1 190 | assert res[0].path == "subdir" 191 | 192 | # test move subdir 193 | os.rename(os.path.join(sub_dir, "t.md"), os.path.join(tmpd, "z.md")) 194 | time.sleep(2) 195 | res, total, pages, _ = s.search("something", 1) 196 | assert total == 1 197 | assert pages == 1 198 | assert len(res) == 1 199 | assert res[0].path == "." 200 | assert res[0].filename == "z.md" 201 | assert "something" in res[0].highlights 202 | 203 | # test remove subdir 204 | os.rename(os.path.join(tmpd, "z.md"), os.path.join(sub_dir, "t.md")) 205 | os.remove(fpath) 206 | time.sleep(2) 207 | res, total, pages, _ = s.search("something", 1) 208 | assert total == 0 209 | assert pages == 0 210 | assert len(res) == 0 211 | 212 | w.stop() 213 | --------------------------------------------------------------------------------