diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7a456be..97c4da2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -20,16 +20,15 @@ jobs: matrix: architecture: - linux/amd64 - # - linux/arm64 steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Docker meta id: meta uses: docker/metadata-action@v4 with: - images: linbreux/wikmd + images: Jerakin/wikmd tags: | type=ref,event=pr type=ref,event=tag @@ -39,9 +38,9 @@ jobs: run: echo "Creating Docker image ${{ steps.meta.outputs.tags }}" - name: Build & export - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: - file: docker/Dockerfile + file: Dockerfile push: false tags: ${{ steps.meta.outputs.tags }} outputs: type=docker,dest=/tmp/wikmd.tar @@ -62,7 +61,7 @@ jobs: name: "test-${{ matrix.tag }}" steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Download artifact uses: actions/download-artifact@v2 @@ -96,16 +95,20 @@ jobs: - name: Assert wikmd status run: curl -I localhost:5000 2>&1 | awk '/HTTP\// {print $2}' | grep -w "200\|301" + # Check that wikmd is rendering + - name: Check wikmd rendering status + run: curl -s localhost:5000 | grep -w "What is it?" + publish: # Publish if official repo and push to 'main' or new tag if: | - github.repository == 'Linbreux/wikmd' && + github.repository == 'Jerakin/wikmd' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) runs-on: ubuntu-latest needs: [build, test] steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub uses: docker/login-action@v2 @@ -114,8 +117,9 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Publish - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: - file: docker/Dockerfile + file: Dockerfile + platforms: linux/amd64,linux/arm64 push: true tags: ${{ needs.build.outputs.wikmd_tags }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1db1fe9..00310d2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,15 +21,16 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install .[test] - name: Test with pytest run: | - python -m pytest -v + python -m pytest - name: Start wikmd - run: python wiki.py & - + run: | + cd src + python -m wikmd.wiki & + - name: screenshots-ci-action uses: flameddd/screenshots-ci-action@master with: diff --git a/.gitignore b/.gitignore index a793919..ce8f20e 100644 --- a/.gitignore +++ b/.gitignore @@ -96,15 +96,12 @@ dmypy.json # Cython debug symbols cython_debug/ +# Generated plugin files +src/wikmd/plugins/draw/drawings/* +!src/wikmd/plugins/draw/drawings/.gitkeep + + # Personal wiki pages wiki/* -wiki/src/* - -!wiki/Features.md -!wiki/How to use the wiki.md -!wiki/Markdown cheatsheet.md -!wiki/Using the version control system.md -!wiki/homepage.md -!wiki/src temp/* diff --git a/docker/README.md b/DOCKER.md similarity index 91% rename from docker/README.md rename to DOCKER.md index 49f276d..e4c1732 100644 --- a/docker/README.md +++ b/DOCKER.md @@ -2,10 +2,6 @@ [wikmd](https://github.com/Linbreux/wikmd) is a file based wiki that uses markdown. -This repo provides Docker files that are loosely based on those of the [linuxserver](https://www.linuxserver.io/) community. - -Docker files are available for arm, arm64 and amd64. - ## Usage Here are some example snippets to help you get started creating a container. @@ -13,7 +9,7 @@ Here are some example snippets to help you get started creating a container. Build the image, ```bash -docker build -t linbreux/wikmd:latest -f docker/Dockerfile . +docker build -t linbreux/wikmd:latest -f Dockerfile . ``` ### docker-compose (recommended, [click here for more info](https://docs.linuxserver.io/general/docker-compose)) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f1832e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,66 @@ +FROM python:3.9-alpine3.17 as python-base + +# Prevents Python from writing pyc files. +ENV PYTHONDONTWRITEBYTECODE=1 + +# Keeps Python from buffering stdout and stderr to avoid situations where +# the application crashes without emitting any logs due to buffering. +ENV PYTHONUNBUFFERED=1 + +# We will be installing venv +ENV VIRTUAL_ENV="/venv" + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser + + +# Add project path to python path, this to ensure we can reach it from anywhere +WORKDIR /code +ENV PYTHONPATH="/code:$PYTHONPATH" + +# prepare the virtual env +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +RUN python -m venv $VIRTUAL_ENV + +# BUILDER +FROM python-base as python-builder + +# Install our dependencies +RUN apk update +RUN apk add git +RUN apk add pandoc +RUN apk add build-base linux-headers + +# Python dependencies +WORKDIR /code + +COPY pyproject.toml /code + +# Copy the py project and use a package to convert our pyproject.toml file into a requirements file +# We can not install the pyproject with pip as that would install the project and we only +# wants to install the project dependencies. +RUN python -m pip install toml-to-requirements==0.2.0 +RUN toml-to-req --toml-file pyproject.toml + +RUN python -m pip install --no-cache-dir --upgrade -r ./requirements.txt + +# Copy our source content over +COPY ./src/wikmd /code/wikmd + +# Change the directory to the root. +WORKDIR / + +# Expose the port that the application listens on. +EXPOSE 5000 + +# Run the application. +CMD ["python", "-m", "wikmd.wiki"] diff --git a/README.md b/README.md index fc58c08..69a947f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ It’s a file-based wiki that aims to simplicity. Instead of storing the data in To view the documents in the browser, the document is converted to html. -![preview](static/images/readme-img.png) +![preview](src/wikmd/static/images/readme-img.png) ## Features @@ -31,6 +31,10 @@ To view the documents in the browser, the document is converted to html. Detailed installation instruction can be found [here](https://linbreux.github.io/wikmd/installation.html). +## Development + +Instructions on the easiest way to develop on the project can be found [here](https://linbreux.github.io/wikmd/development.html). + ## Plugins & Knowledge graph (beta) More info can be found in the [docs](https://linbreux.github.io/wikmd/knowledge%20graph.html). diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 0f57069..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -FROM ghcr.io/linuxserver/baseimage-ubuntu:jammy - -COPY . /app/wikmd - -RUN \ - echo "**** install wikmd dependencies ****" && \ - apt-get update -y && \ - apt-get install -y python3-pip python3-dev pandoc git && \ - # echo "**** install wikmd ****" && \ - # WIKMD_RELEASE=$(curl -sX GET https://api.github.com/repos/Linbreux/wikmd/releases/latest \ - # | awk '/tag_name/{print $4;exit}' FS='[""]' | sed 's|^v||') && \ - # mkdir -p /app/wikmd && \ - # curl -o \ - # /tmp/wikmd.tar.gz -L \ - # https://github.com/Linbreux/wikmd/archive/master.tar.gz && \ - # #"https://github.com/Linbreux/wikmd/archive/refs/tags/v${WIKMD_RELEASE}.tar.gz" && \ - # tar xf /tmp/wikmd.tar.gz -C \ - # /app/wikmd --strip-components=1 && \ - # cp -R . /app/wikmd && \ - echo "**** install pip requirements ****" && \ - cd /app/wikmd && \ - pip3 install -r requirements.txt && \ - echo "**** cleanup ****" && \ - apt-get -y purge \ - python3-pip && \ - apt-get -y autoremove && \ - rm -rf \ - /tmp/* \ - /var/lib/apt/lists/* \ - /var/tmp/* \ - /root/.cache - -COPY docker/root/ / - -ENV LANG=C.UTF-8 -ENV HOME=/wiki - -# ports and volumes -EXPOSE 5000 diff --git a/docker/Dockerfile.aarch64 b/docker/Dockerfile.aarch64 deleted file mode 100644 index bb7658e..0000000 --- a/docker/Dockerfile.aarch64 +++ /dev/null @@ -1,38 +0,0 @@ -FROM ghcr.io/linuxserver/baseimage-ubuntu:arm64v8-jammy - -COPY . /app/wikmd - -RUN \ - echo "**** install wikmd dependencies ****" && \ - apt-get update -y && \ - apt-get install -y python3-pip python3-dev pandoc git libxml2-dev libxslt1-dev && \ - # echo "**** install wikmd ****" && \ - # WIKMD_RELEASE=$(curl -sX GET https://api.github.com/repos/Linbreux/wikmd/releases/latest \ - # | awk '/tag_name/{print $4;exit}' FS='[""]' | sed 's|^v||') && \ - # mkdir -p /app/wikmd && \ - # curl -o \ - # /tmp/wikmd.tar.gz -L \ - # https://github.com/Linbreux/wikmd/archive/master.tar.gz && \ - # #"https://github.com/Linbreux/wikmd/archive/refs/tags/v${WIKMD_RELEASE}.tar.gz" && \ - # tar xf /tmp/wikmd.tar.gz -C \ - # /app/wikmd --strip-components=1 && \ - echo "**** install pip requirements ****" && \ - cd /app/wikmd && \ - pip3 install -r requirements.txt && \ - echo "**** cleanup ****" && \ - apt-get -y purge \ - python3-pip && \ - apt-get -y autoremove && \ - rm -rf \ - /tmp/* \ - /var/lib/apt/lists/* \ - /var/tmp/* \ - /root/.cache - -COPY docker/root/ / - -ENV LANG=C.UTF-8 -ENV HOME=/wiki - -# ports and volumes -EXPOSE 5000 diff --git a/docker/Dockerfile.armhf b/docker/Dockerfile.armhf deleted file mode 100644 index b2ab826..0000000 --- a/docker/Dockerfile.armhf +++ /dev/null @@ -1,38 +0,0 @@ -FROM ghcr.io/linuxserver/baseimage-ubuntu:arm32v7-jammy - -COPY . /app/wikmd - -RUN \ - echo "**** install wikmd dependencies ****" && \ - apt-get update -y && \ - apt-get install -y python3-pip python3-dev pandoc git libxml2-dev libxslt1-dev && \ - # echo "**** install wikmd ****" && \ - # WIKMD_RELEASE=$(curl -sX GET https://api.github.com/repos/Linbreux/wikmd/releases/latest \ - # | awk '/tag_name/{print $4;exit}' FS='[""]' | sed 's|^v||') && \ - # mkdir -p /app/wikmd && \ - # curl -o \ - # /tmp/wikmd.tar.gz -L \ - # https://github.com/Linbreux/wikmd/archive/master.tar.gz && \ - # #"https://github.com/Linbreux/wikmd/archive/refs/tags/v${WIKMD_RELEASE}.tar.gz" && \ - # tar xf /tmp/wikmd.tar.gz -C \ - # /app/wikmd --strip-components=1 && \ - echo "**** install pip requirements ****" && \ - cd /app/wikmd && \ - pip3 install -r requirements.txt && \ - echo "**** cleanup ****" && \ - apt-get -y purge \ - python3-pip && \ - apt-get -y autoremove && \ - rm -rf \ - /tmp/* \ - /var/lib/apt/lists/* \ - /var/tmp/* \ - /root/.cache - -COPY docker/root/ / - -ENV LANG=C.UTF-8 -ENV HOME=/wiki - -# ports and volumes -EXPOSE 5000 diff --git a/docker/root/etc/cont-init.d/30-config b/docker/root/etc/cont-init.d/30-config deleted file mode 100644 index 91710bb..0000000 --- a/docker/root/etc/cont-init.d/30-config +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/with-contenv bash - -# Create log file -if [ ! -f "/var/log/wikmd.log" ] -then - touch /var/log/wikmd.log - chown abc:abc /var/log/wikmd.log - chmod 666 /var/log/wikmd.log -fi - -# if /wiki isn't mounted create it -if [ ! -d "/wiki" ] -then - # create directories - mkdir -p /wiki - chown -R abc:abc /wiki - chown -R abc:abc /wiki/* -fi - -# If /wiki exists and is empty, populate it with the examples -if [ -d "/wiki" ] && [ ! "$(ls -A /wiki)" ] -then - # copy examples - cp /app/wikmd/wiki/*.md /wiki/. - - # permissions - chown -R abc:abc /wiki/* - chown -R abc:abc /wiki -fi - -chown -R abc:abc /app/wikmd/plugins/* -chown -R abc:abc /app/wikmd/plugins -chown -R abc:abc /app/wikmd/static/* -chown -R abc:abc /app/wikmd/static diff --git a/docker/root/etc/services.d/wikmd/run b/docker/root/etc/services.d/wikmd/run deleted file mode 100644 index adead31..0000000 --- a/docker/root/etc/services.d/wikmd/run +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/with-contenv bash -export WIKI_DIRECTORY='/wiki' -export WIKMD_LOGGING_FILE='/var/log/wikmd.log' - -exec s6-setuidgid abc python3 /app/wikmd/wiki.py diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..e899a01 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,36 @@ +--- +layout: default +title: Development +parent: Installation +nav_order: 12 +--- + +# Regular installation +! It's tested on windows and linux based systems. +! Runs on flask server + +Clone the repository +``` +git clone https://github.com/Linbreux/wikmd.git +``` + +cd in wikmd +``` +cd wikmd +``` + +Create a virtual env and activate it (optional, but highly recommended) +``` +virtualenv venv +source venv/bin/activate +``` + +Install it in [development mode aka editable install](https://setuptools.pypa.io/en/latest/userguide/development_mode.html) +``` +python -m pip install .[dev] --editable +``` + +Run the wiki +``` +python -m wikmd.wiki +``` diff --git a/docs/installation/docker.md b/docs/installation/docker.md index 7cffd43..d55c9ca 100644 --- a/docs/installation/docker.md +++ b/docs/installation/docker.md @@ -5,14 +5,6 @@ parent: Installation nav_order: 2 --- -# wikmd Docker image - -[wikmd](https://github.com/Linbreux/wikmd) is a file based wiki that uses Markdown. - -This repo provides Docker files that are loosely based on those of the [linuxserver](https://www.linuxserver.io/) community. - -Docker files are available for various architectures, including arm (`Dockerfile.armhf`), arm64 (`Dockerfile.aarch64`), and amd64 (`Dockerfile`). - ## Usage Here are some example snippets to help you get started creating a container. @@ -27,7 +19,7 @@ Or, build the image after cloning the source code itself: ```bash git clone https://github.com/linbreux/wikmd.git && cd wikmd -docker build -t linbreux/wikmd:latest -f docker/Dockerfile . +docker build -t linbreux/wikmd:latest -f Dockerfile . ``` ### docker-compose (recommended, [click here for more info](https://docs.linuxserver.io/general/docker-compose)) @@ -69,16 +61,16 @@ docker run -d \ 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. -| Parameter | Function | -| :----: | --- | -| `-p 5000` | Port for wikmd webinterface. | -| `-e PUID=1000` | for UserID - see below for explanation | -| `-e PGID=1000` | for GroupID - see below for explanation | -| `-e TZ=Europe/Paris` | Specify a timezone to use EG Europe/Paris | -| `-e HOMEPAGE=homepage.md` | Specify the file to use as a homepage | -| `-e HOMEPAGE_TITLE=title` | Specify the homepage's title | -| `-e WIKMD_LOGGING=1` | Enable/disable file logging | -| `-v /wiki` | Path to the file-based wiki. | +| Parameter | Function | +|:--------------------------|-------------------------------------------| +| `-p 5000` | Port for wikmd webinterface. | +| `-e PUID=1000` | for UserID - see below for explanation | +| `-e PGID=1000` | for GroupID - see below for explanation | +| `-e TZ=Europe/Paris` | Specify a timezone to use EG Europe/Paris | +| `-e HOMEPAGE=homepage.md` | Specify the file to use as a homepage | +| `-e HOMEPAGE_TITLE=title` | Specify the homepage's title | +| `-e WIKMD_LOGGING=1` | Enable/disable file logging | +| `-v /wiki` | Path to the file-based wiki. | ## User / Group Identifiers diff --git a/docs/installation/regular_install.md b/docs/installation/regular_install.md index 2d57f5f..b7e438d 100644 --- a/docs/installation/regular_install.md +++ b/docs/installation/regular_install.md @@ -18,23 +18,22 @@ cd in wikmd cd wikmd ``` -Create a virtual env and activate it(optional) +Create a virtual env and activate it (optional, but highly recommended) ``` virtualenv venv source venv/bin/activate ``` -Install requirements -``` -pip install -r requirements.txt -``` -Run the wiki + +Install ``` -export FLASK_APP=wiki.py -flask run --host=0.0.0.0 +python -m pip install . ``` -or + +Run the wiki, remember that all paths within wikmd are defined as relative. +That means that your current working directory will be the base of the project. +As such your current directory will hold the `wiki` directory that contains all md files. ``` -python wiki.py +python -m wikmd.wiki ``` 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. @@ -47,7 +46,8 @@ Maybe you need to install pandoc on your system before this works. ``` sudo apt-get update && sudo apt-get install pandoc ``` -You may experience an issue when running `pip install -r requirements.txt` where you receive the following error: + +You may experience an issue when running `python -m pip install -r requirements.txt` where you receive the following error: ``` psutil/_psutil_common.c:9:10: fatal error: Python.h: No such file or directory 9 | #include @@ -70,7 +70,7 @@ sudo dnf install python3-devel ``` For other distros, you can search up `[distro] install python 3 dev`. -You may experience an error when running `pip install -r requirements.txt` where it asks you to install `gcc python3-dev`. Example: +You may experience an error when running `python pip install .` where it asks you to install `gcc python3-dev`. Example: ``` unable to execute 'x86_64-linux-gnu-gcc': No such file or directory C compiler or Python headers are not installed on this system. Try to run: @@ -97,8 +97,7 @@ After=network.target [Service] User= WorkingDirectory= -Environment=FLASK_APP=wiki.py -ExecStart=/env/bin/python3 wiki.py +ExecStart=/env/bin/python3 -m wikmd.wiki Restart=always [Install] @@ -125,7 +124,7 @@ To fix, run the following commands: sudo su cd ~ umask 022 -pip install -r /requirements.txt +pip install -r . ``` This will install the python packages system-wide, allowing the wiki service to access it. @@ -137,7 +136,7 @@ Run `systemctl restart wiki.service` and it should be working. You should install [pandoc](https://pandoc.org/installing.html) on your windows system. Now you should be able to start the server. ``` -python wiki.py +python -m wikmd.wiki ``` 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). ``` diff --git a/plugins/draw/draw.py b/plugins/draw/draw.py deleted file mode 100644 index 49b8372..0000000 --- a/plugins/draw/draw.py +++ /dev/null @@ -1,101 +0,0 @@ -import uuid -import os -import re -import shutil -from flask import Flask -from config import WikmdConfig - -class Plugin: - def import_head(self): - return "" - - def __init__(self, flask_app: Flask, config: WikmdConfig, web_dep): - self.name = "DrawIO integration" - self.plugname = "draw" - self.flask_app = flask_app - self.config = config - self.this_location = os.path.dirname(__file__) - self.web_dep = web_dep - - def get_plugin_name(self) -> str: - """ - returns the name of the plugin - """ - return self.name - - def process_md(self, md: str) -> str: - """ - returns the md file after process the input file - """ - return self.search_for_pattern_and_replace_with_uniqueid(md) - - def process_html(self, html: str) -> str: - """ - returns the html file after process the input file - """ - return self.search_in_html_for_draw(html) - - def communicate_plugin(self, request): - """ - communication from "/plug_com" - """ - id = request.form['id'] - image = request.form['image'] - - self.flask_app.logger.info(f"Plug/{self.name} - changing drawing {id}") - - # look for folder - location = os.path.join(os.path.dirname(__file__),"drawings", id) - if os.path.exists(location): - file = open(location, "w") - file.write(image) - file.close() - - return "ok" - - def look_for_existing_drawid(self, drawid: str) -> str: - """ - look for a drawId in the wiki/draw folder and return the file as a string - """ - try: - file = open(os.path.join(self.this_location,"drawings",drawid),"r") - return file.read() - except Exception: - print("Did not find the file") - return "" - - def create_draw_file(self, filename: str) -> None: - """ - Copy the default drawing to a new one with this filename - """ - path_to_file = os.path.join(self.this_location,"drawings",filename) - shutil.copyfile(os.path.join(self.this_location, "default_draw"), path_to_file) - s = open(path_to_file,"r") - result = re.sub("id=\"\"","id=\"" + filename + "\"",s.read()) - s.close() - s = open(path_to_file,"w") - s.write(result) - s.close() - - - def search_for_pattern_and_replace_with_uniqueid(self, file: str) -> str: - """ - search for [[draw]] and replace with draw_ - """ - filename = "draw_" + str(uuid.uuid4()) - result = re.sub(r"^\[\[draw\]\]", "[[" + filename + "]]", file, flags=re.MULTILINE) - print(file) - self.create_draw_file(filename) - return result - - def search_in_html_for_draw(self,file: str) -> str: - """ - search for [[draw_]] in "file" and replace it with the content of a corresponding drawfile - """ - draws = re.findall(r"\[\[(draw_.*)\]\]", file) - result = file - for draw in draws: - result = re.sub(r"\[\["+draw+r"\]\]", self.look_for_existing_drawid(draw), result) - return result - - diff --git a/plugins/draw/drawings/.gitignore b/plugins/draw/drawings/.gitignore deleted file mode 100644 index f59ec20..0000000 --- a/plugins/draw/drawings/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* \ No newline at end of file diff --git a/plugins/load_plugins.py b/plugins/load_plugins.py deleted file mode 100644 index a8376d7..0000000 --- a/plugins/load_plugins.py +++ /dev/null @@ -1,29 +0,0 @@ -import importlib -from flask import Flask -from config import WikmdConfig - - -class PluginLoader(): - """ - The plugin loader will load all plugins inside "plugins" folder - a plugin should have a folder and a file inside the folder, - both with the name of the plugin - """ - def __init__(self, flask_app: Flask, config: WikmdConfig, web_deps= None, plugins:list=[], ): - # Checking if plugin were sent - if plugins != []: - # create a list of plugins - self._plugins = [ - importlib.import_module(f"plugins.{plugin}.{plugin}",".").Plugin(flask_app, config, web_deps) for plugin in plugins - ] - else: - self._plugins = [] - - for plugin in self._plugins: - print(plugin.get_plugin_name()) - - def get_plugins(self): - """ - returns a list of plugins - """ - return self._plugins \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..56902dc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,100 @@ +[project] +requires-python = ">= 3.8" +name = "wikmd" +description = "A file-based wiki that aims for simplicity." +authors = [ + { name = "linbreux" } +] + +version = "1.9.0" + +dependencies=[ + "Flask==3.0.2", + "GitPython==3.1.42", + "Markdown==3.5.2", + "PyYAML==6.0.1", + "Werkzeug==3.0.1", + "Whoosh==2.7.4", + "beautifulsoup4==4.12.3", + "pandoc-eqnos==2.5.0", + "pandoc-fignos==2.4.0", + "pandoc-secnos==2.2.2", + "pandoc-tablenos==2.3.0", + "pandoc-xnos==2.5.0", + "pandocfilters==1.5.1", + "pypandoc==1.13", + "requests==2.31.0", + "lxml==5.1.0", + "watchdog==2.1.9", + "cachelib==0.12.0", +] + +[project.optional-dependencies] +dev = [ + "pytest", + "ruff", + "ruff-lsp", +] +test = [ + "pytest", +] + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +wikmd = [ + "wikmd-config.yaml", + "plugins/draw/default_draw", + "static/**/*", + "templates/**/*", + "wiki_template/**/*", +] + +[tool.pytest.ini_options] +pythonpath = [ + "src" +] + +[tool.ruff] +src = ["", "tests"] +select = ["ALL"] + +[tool.ruff.lint.per-file-ignores] +"*" = [ + # ANN001: Missing type annotation for public function + "ANN101", + + # ANN204: Missing return type annotation for __init__ + "ANN204", + + # D100: Missing docstring in public module + "D100", + + # D107: Missing docstring in __init__ + "D107", + + # ANN001: Missing type annotation for public function + "ANN101", +] + +"tests/*" = [ + # S101: Check for assert + "S101", + + # ANN001: Missing type annotation for public function + "ANN001", + + # ANN201: Missing return type annotation for public function + "ANN201", + + # D103: Missing docstring in public function + "D103", + + # PLR2004: Magic numbers + "PLR2004", +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7b25c49..0000000 --- a/requirements.txt +++ /dev/null @@ -1,26 +0,0 @@ -Flask==3.0.2 -GitPython==3.1.42 -itsdangerous==2.1.2 -Jinja2==3.1.3 -Markdown==3.5.2 -MarkupSafe==2.1.5 -PyYAML==6.0.1 -Werkzeug==3.0.1 -Whoosh==2.7.4 -beautifulsoup4==4.12.3 -click==8.1.7 -gitdb==4.0.11 -idna==3.6 -pandoc-eqnos==2.5.0 -pandoc-fignos==2.4.0 -pandoc-secnos==2.2.2 -pandoc-tablenos==2.3.0 -pandoc-xnos==2.5.0 -pandocfilters==1.5.1 -pypandoc==1.13 -requests==2.31.0 -smmap==5.0.1 -lxml==5.1.0 -watchdog==2.1.9 -cachelib==0.12.0 -psutil==5.9.8 diff --git a/__init__.py b/src/wikmd/__init__.py similarity index 100% rename from __init__.py rename to src/wikmd/__init__.py diff --git a/cache.py b/src/wikmd/cache.py similarity index 100% rename from cache.py rename to src/wikmd/cache.py diff --git a/config.py b/src/wikmd/config.py similarity index 99% rename from config.py rename to src/wikmd/config.py index 83b4363..0d50e53 100644 --- a/config.py +++ b/src/wikmd/config.py @@ -1,4 +1,5 @@ import os + import yaml WIKMD_CONFIG_FILE = "wikmd-config.yaml" diff --git a/git_manager.py b/src/wikmd/git_manager.py similarity index 96% rename from git_manager.py rename to src/wikmd/git_manager.py index 2590e27..d832757 100644 --- a/git_manager.py +++ b/src/wikmd/git_manager.py @@ -1,13 +1,11 @@ -import os import datetime - +import os from typing import Optional -from flask import Flask -from git import Repo, InvalidGitRepositoryError, GitCommandError, NoSuchPathError - -from config import WikmdConfig -from utils import move_all_files +from flask import Flask +from git import GitCommandError, InvalidGitRepositoryError, NoSuchPathError, Repo +from wikmd.config import WikmdConfig +from wikmd.utils import move_all_files TEMP_DIR = "temp" @@ -37,12 +35,14 @@ def __init__(self, flask_app: Flask): self.wiki_directory = cfg.wiki_directory self.sync_with_remote = cfg.sync_with_remote - if not os.path.exists(self.wiki_directory): - os.mkdir(self.wiki_directory) self.remote_url = cfg.remote_url - self.repo: Optional[Repo] = None + + def initialize(self): + if not os.path.exists(self.wiki_directory): + self.flask_app.logger.warning("wiki directory doesn't exist") + return self.__git_repo_init() def __git_repo_init(self): diff --git a/image_manager.py b/src/wikmd/image_manager.py similarity index 98% rename from image_manager.py rename to src/wikmd/image_manager.py index 10aa385..2e55d64 100644 --- a/image_manager.py +++ b/src/wikmd/image_manager.py @@ -5,7 +5,7 @@ from base64 import b32encode from hashlib import sha1 -from werkzeug.utils import secure_filename, safe_join +from werkzeug.utils import safe_join, secure_filename class ImageManager: @@ -64,7 +64,7 @@ def cleanup_images(self): """Deletes images not used by any page""" saved_images = set(os.listdir(self.images_path)) # Don't delete .gitignore - saved_images.discard(".gitignore") + saved_images.discard(".gitkeep") # Matches [*](/img/*) it does not matter if images_route is "/img" or "img" image_link_pattern = fr"\[(.*?)\]\(({os.path.join('/', self.cfg.images_route)}.+?)\)" diff --git a/knowledge_graph.py b/src/wikmd/knowledge_graph.py similarity index 98% rename from knowledge_graph.py rename to src/wikmd/knowledge_graph.py index af24e1d..4b6d9ce 100644 --- a/knowledge_graph.py +++ b/src/wikmd/knowledge_graph.py @@ -2,7 +2,7 @@ import re from urllib.parse import unquote -from config import WikmdConfig +from wikmd.config import WikmdConfig cfg = WikmdConfig() diff --git a/src/wikmd/plugin_manager.py b/src/wikmd/plugin_manager.py new file mode 100644 index 0000000..19235a6 --- /dev/null +++ b/src/wikmd/plugin_manager.py @@ -0,0 +1,46 @@ +import importlib + +from flask import Flask +from wikmd.config import WikmdConfig + + +class PluginManager: + """Load all plugins inside "plugins" folder. + + A plugin needs to be a package with a module with the same name. + """ + def __init__(self, flask_app: Flask, config: WikmdConfig, web_deps=None, plugins: list = None): + plugins = [] if plugins is None else plugins + self.plugins = {} + + self.config = config + self.flask_app = flask_app + self.web_deps = web_deps + + # Checking if plugin were sent + if not plugins: + return + + for plugin in plugins: + self.plugins[plugin] = self.load_plugin(plugin) + + def send(self, plugin, slot, data): + """Send a message to a single plugin and get the result.""" + if plugin not in self.plugins: + return data + if slot in dir(plugin): + self.flask_app.logger.info("Plugin %s ran on %s", plugin, slot) + plugin_obj = self.plugins[plugin] + data = getattr(plugin_obj, slot)(data) + return data + + def broadcast(self, slot, data): + """Broadcast the message to each plugin and pass the data to them in turn. Return the resulting data.""" + for name in self.plugins: + data = self.send(name, slot, data) + return data + + def load_plugin(self, plugin): + return (importlib.import_module( + f"wikmd.plugins.{plugin}.{plugin}", ".") + .Plugin(self.flask_app, self.config, self.web_deps)) diff --git a/plugins/__init__.py b/src/wikmd/plugins/__init__.py similarity index 100% rename from plugins/__init__.py rename to src/wikmd/plugins/__init__.py diff --git a/plugins/alerts/__init__.py b/src/wikmd/plugins/alerts/__init__.py similarity index 100% rename from plugins/alerts/__init__.py rename to src/wikmd/plugins/alerts/__init__.py diff --git a/plugins/alerts/alerts.py b/src/wikmd/plugins/alerts/alerts.py similarity index 97% rename from plugins/alerts/alerts.py rename to src/wikmd/plugins/alerts/alerts.py index b978503..0cbfff4 100644 --- a/plugins/alerts/alerts.py +++ b/src/wikmd/plugins/alerts/alerts.py @@ -1,9 +1,9 @@ -import uuid import os import re -import shutil + from flask import Flask -from config import WikmdConfig +from wikmd.config import WikmdConfig + class Plugin: def __init__(self, flask_app: Flask, config: WikmdConfig, web_dep ): diff --git a/plugins/draw/__init__.py b/src/wikmd/plugins/draw/__init__.py similarity index 100% rename from plugins/draw/__init__.py rename to src/wikmd/plugins/draw/__init__.py diff --git a/plugins/draw/default_draw b/src/wikmd/plugins/draw/default_draw similarity index 100% rename from plugins/draw/default_draw rename to src/wikmd/plugins/draw/default_draw diff --git a/src/wikmd/plugins/draw/draw.py b/src/wikmd/plugins/draw/draw.py new file mode 100644 index 0000000..4d2e5f2 --- /dev/null +++ b/src/wikmd/plugins/draw/draw.py @@ -0,0 +1,177 @@ +import re +import uuid +from pathlib import Path + + +from flask import Flask +from wikmd.config import WikmdConfig + +default_draw = ('') + + +class Plugin: + @staticmethod + def import_head(): + return "" + + def __init__(self, flask_app: Flask, config: WikmdConfig, web_dep): + self.name = "DrawIO integration" + self.plugname = "draw" + self.flask_app = flask_app + self.config = config + self.web_dep = web_dep + self.save_location = Path(self.config.wiki_directory) / ".plugin-draw" + config.hide_folder_in_wiki.append(".plugin-draw") + self.save_location.mkdir(exist_ok=True) + + def get_plugin_name(self) -> str: + """ + returns the name of the plugin + """ + return self.name + + def process_md(self, md: str) -> str: + """ + returns the md file after process the input file + """ + return self.search_for_pattern_and_replace_with_uniqueid(md) + + def process_html(self, html: str) -> str: + """ + returns the html file after process the input file + """ + return self.search_in_html_for_draw(html) + + def communicate_plugin(self, request): + """ + communication from "/plug_com" + """ + id_ = request.form['id'] + image = request.form['image'] + + self.flask_app.logger.info(f"Plug/{self.name} - changing drawing {id_}") + + # look for folder + location = self.save_location / id_ + if not location.exists(): + return "ok" + with location.open("w") as fp: + fp.write(image) + + def look_for_existing_draw_id(self, draw_id: str) -> str: + """ + look for a drawId in the wiki/draw folder and return the file as a string + """ + location = self.save_location / draw_id + if not location.exists(): + self.flask_app.logger.info(f"Plug/{self.name} - Could not find file with ID %s", draw_id) + return "" + + with location.open() as fp: + data = fp.read() + return data + + def create_draw_file(self, filename: str) -> None: + """Create a default drawing at filename""" + location = self.save_location / filename + with location.open("w") as fp: + draw = default_draw.replace('id=""', f'id="{filename}"') + fp.write(draw) + + def search_for_pattern_and_replace_with_uniqueid(self, md: str) -> str: + """ + search for [[draw]] and replace with draw_ + """ + filename = "draw_" + str(uuid.uuid4()) + result = md.replace("[[draw]]", f"[[{filename}]]") + self.create_draw_file(filename) + return result + + def search_in_html_for_draw(self, html: str) -> str: + """Search for [[draw_]] in the html string and replace it + with the content of a corresponding drawfile + """ + draws = re.findall(r"\[\[(draw_.*)]]", html) + result = html + for draw in draws: + result = re.sub(fr"\[\[{draw}]]", self.look_for_existing_draw_id(draw), result) + return result diff --git a/plugins/embed-pages/__init__.py b/src/wikmd/plugins/draw/drawings/.gitkeep similarity index 100% rename from plugins/embed-pages/__init__.py rename to src/wikmd/plugins/draw/drawings/.gitkeep diff --git a/plugins/mermaid/__init__.py b/src/wikmd/plugins/embed-pages/__init__.py similarity index 100% rename from plugins/mermaid/__init__.py rename to src/wikmd/plugins/embed-pages/__init__.py diff --git a/plugins/embed-pages/embed-pages.py b/src/wikmd/plugins/embed-pages/embed-pages.py similarity index 96% rename from plugins/embed-pages/embed-pages.py rename to src/wikmd/plugins/embed-pages/embed-pages.py index b4e2ae0..3af63a5 100644 --- a/plugins/embed-pages/embed-pages.py +++ b/src/wikmd/plugins/embed-pages/embed-pages.py @@ -1,9 +1,9 @@ -import uuid import os import re -import shutil + from flask import Flask -from config import WikmdConfig +from wikmd.config import WikmdConfig + class Plugin: def __init__(self, flask_app: Flask, config: WikmdConfig, web_dep): diff --git a/static/js/.empty b/src/wikmd/plugins/mermaid/__init__.py similarity index 100% rename from static/js/.empty rename to src/wikmd/plugins/mermaid/__init__.py diff --git a/plugins/mermaid/mermaid.py b/src/wikmd/plugins/mermaid/mermaid.py similarity index 90% rename from plugins/mermaid/mermaid.py rename to src/wikmd/plugins/mermaid/mermaid.py index cf82130..f92dd91 100644 --- a/plugins/mermaid/mermaid.py +++ b/src/wikmd/plugins/mermaid/mermaid.py @@ -1,10 +1,6 @@ -import uuid import os -import re -import shutil from flask import Flask -from config import WikmdConfig -from web_dependencies import WEB_DEPENDENCIES +from wikmd.config import WikmdConfig injected_script = """ +{% endblock %} + +{% block content %} + +

{{ info|safe }}

+

{{ modif }}

+
+ +
+ + + + +{% endblock %} + +{% block scripts %} + + +{% endblock %} diff --git a/src/wikmd/templates/index.html b/src/wikmd/templates/index.html new file mode 100644 index 0000000..a56863b --- /dev/null +++ b/src/wikmd/templates/index.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{%block content%} + + +

+ {{form.content|safe}} +

+{%endblock%} diff --git a/templates/knowledge-graph.html b/src/wikmd/templates/knowledge-graph.html similarity index 98% rename from templates/knowledge-graph.html rename to src/wikmd/templates/knowledge-graph.html index c55e8ef..3e1aca2 100644 --- a/templates/knowledge-graph.html +++ b/src/wikmd/templates/knowledge-graph.html @@ -1,4 +1,4 @@ -{% extends 'base.html'%} +{% extends 'base.html' %} {%block head%} diff --git a/templates/list_files.html b/src/wikmd/templates/list_files.html similarity index 81% rename from templates/list_files.html rename to src/wikmd/templates/list_files.html index 4c541b5..d88c99e 100644 --- a/templates/list_files.html +++ b/src/wikmd/templates/list_files.html @@ -5,12 +5,12 @@

{{ folder }} ALL FILES

  • ..
  • - {% for i in list %} + {% for i in list.children %}
  • - {% if i.folder != "" and i.folder != folder %} - {{ i.folder }}/ + {% if i.children %} + {{ i.name }}/ {% else %} - {{ i.doc }} + {{ i.name }} {% endif %}
  • {% endfor %} diff --git a/templates/login.html b/src/wikmd/templates/login.html similarity index 95% rename from templates/login.html rename to src/wikmd/templates/login.html index 670edf2..f572d81 100644 --- a/templates/login.html +++ b/src/wikmd/templates/login.html @@ -1,4 +1,4 @@ -{% extends 'base.html'%} +{% extends 'base.html' %} {%block content%} diff --git a/templates/new.html b/src/wikmd/templates/new.html similarity index 90% rename from templates/new.html rename to src/wikmd/templates/new.html index d457bf8..33ad1af 100644 --- a/templates/new.html +++ b/src/wikmd/templates/new.html @@ -4,12 +4,12 @@ {%block head%} - + {% if system.darktheme == True %} - - + + {% endif %} {%endblock%} @@ -20,12 +20,12 @@
    - +
    - +

    diff --git a/templates/search.html b/src/wikmd/templates/search.html similarity index 89% rename from templates/search.html rename to src/wikmd/templates/search.html index 489e2a5..e582a53 100644 --- a/templates/search.html +++ b/src/wikmd/templates/search.html @@ -63,7 +63,7 @@

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

    Did you mean?: {% for term in suggestions %} - {{ term }} + {{ term }} {% if not loop.last %}, {% endif %} {% endfor %}

      @@ -78,7 +78,7 @@

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

        {% for page in range(1, num_pages + 1) %} {% endfor %}
          diff --git a/src/wikmd/templates/sidebar.html b/src/wikmd/templates/sidebar.html new file mode 100644 index 0000000..526e10e --- /dev/null +++ b/src/wikmd/templates/sidebar.html @@ -0,0 +1,40 @@ +{% macro build_element(item) -%} + {% if item.name == "wiki" %} + {# Skip the root folder #} +
            + {% for child in item.children %} +
          • + {{ build_element(child) }} +
          • + {% endfor %} +
          + + {% else %} + {% if item.children %} + +
          +
            + {% for child in item.children %} +
          • + {{ build_element(child) }} +
          • + {% endfor %} +
          +
          + {% else %} + {{ item.name }} + {% endif %} + {% endif %} +{%- endmacro %} + + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/src/wikmd/utils.py b/src/wikmd/utils.py new file mode 100644 index 0000000..5a44db4 --- /dev/null +++ b/src/wikmd/utils.py @@ -0,0 +1,107 @@ +import os +import unicodedata +import re + +_filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9 _.-]") +_windows_device_files = { + "CON", + "PRN", + "AUX", + "NUL", + *(f"COM{i}" for i in range(10)), + *(f"LPT{i}" for i in range(10)), +} + + +def secure_filename(filename: str) -> str: + """Convert your filename to be safe for the os. + + Function from werkzeug. Changed to allow space in the file name. + """ + filename = unicodedata.normalize("NFKD", filename) + filename = filename.encode("ascii", "ignore").decode("ascii") + for sep in os.sep, os.path.altsep: + if sep: + filename = filename.replace(sep, "_") + filename = str(_filename_ascii_strip_re.sub("", filename)).strip( + "._" + ) + # on nt a couple of special files are present in each folder. We + # have to ensure that the target file is not such a filename. In + # this case we prepend an underline + if ( + os.name == "nt" + and filename + and filename.split(".")[0].upper() in _windows_device_files + ): + filename = f"_{filename}" + + return filename + + +_windows_device_files = { + "CON", + "PRN", + "AUX", + "NUL", + *(f"COM{i}" for i in range(10)), + *(f"LPT{i}" for i in range(10)), +} + + +def secure_filename(filename: str) -> str: + """Copied from werkzeug. This one allows space in the file name.""" + + filename = unicodedata.normalize("NFKD", filename) + filename = filename.encode("ascii", "ignore").decode("ascii") + for sep in os.sep, os.path.altsep: + if sep: + filename = filename.replace(sep, "_") + filename = filename.strip("._") + # on nt a couple of special files are present in each folder. We + # have to ensure that the target file is not such a filename. In + # this case we prepend an underline + if ( + os.name == "nt" + and filename + and filename.split(".")[0].upper() in _windows_device_files + ): + filename = f"_{filename}" + + return filename + +def pathify(path1, path2): + """ + Joins two paths and eventually converts them from Win (\\) to linux OS separator. + :param path1: first path + :param path2: second path + :return safe joined path + """ + return os.path.join(path1, path2).replace("\\", "/") + + +def move_all_files(src_dir: str, dest_dir: str): + """ + Function that moves all the files from a source directory to a destination one. + If a file with the same name is already present in the destination, the source file will be renamed with a + '-copy-XX' suffix. + :param src_dir: source directory + :param dest_dir: destination directory + """ + if not os.path.isdir(dest_dir): + os.mkdir(dest_dir) # make the dir if it doesn't exist + + src_files = os.listdir(src_dir) + dest_files = os.listdir(dest_dir) + + for file in src_files: + new_file = file + copies_count = 1 + while new_file in dest_files: # if the file is already present, append '-copy-XX' to the file name + file_split = file.split('.') + new_file = f"{file_split[0]}-copy-{copies_count}" + if len(file_split) > 1: # if the file has an extension (it's not a directory nor a file without extension) + new_file += f".{file_split[1]}" # add the extension + copies_count += 1 + + os.rename(f"{src_dir}/{file}", f"{dest_dir}/{new_file}") \ No newline at end of file diff --git a/web_dependencies.py b/src/wikmd/web_dependencies.py similarity index 100% rename from web_dependencies.py rename to src/wikmd/web_dependencies.py diff --git a/src/wikmd/wiki.py b/src/wikmd/wiki.py new file mode 100644 index 0000000..483da38 --- /dev/null +++ b/src/wikmd/wiki.py @@ -0,0 +1,730 @@ +from __future__ import annotations + +import logging +import os +import secrets +import shutil +import time +import uuid +from hashlib import sha256 +from pathlib import Path +from threading import Thread +from typing import TypeVar + +import pypandoc +from flask import ( + Flask, + Response, + flash, + make_response, + redirect, + render_template, + request, + send_file, + send_from_directory, + url_for, +) +from lxml.html.clean import clean_html +from werkzeug.utils import safe_join + +from wikmd import knowledge_graph +from wikmd.cache import Cache +from wikmd.config import WikmdConfig +from wikmd.git_manager import WikiRepoManager +from wikmd.image_manager import ImageManager +from wikmd.plugin_manager import PluginManager +from wikmd.search import Search, Watchdog +from wikmd.utils import pathify, secure_filename +from wikmd.web_dependencies import get_web_deps + +PC_T = TypeVar("PC_T", bound="PageContent") + +SESSIONS = [] + +cfg = WikmdConfig() + +UPLOAD_FOLDER_PATH = pathify(cfg.wiki_directory, cfg.images_route) +GIT_FOLDER_PATH = pathify(cfg.wiki_directory, ".git") +HIDDEN_FOLDER_PATH_LIST = [pathify(cfg.wiki_directory, hidden_folder) + for hidden_folder in cfg.hide_folder_in_wiki] +HOMEPAGE_PATH = pathify(cfg.wiki_directory, cfg.homepage) +HIDDEN_PATHS = (UPLOAD_FOLDER_PATH, GIT_FOLDER_PATH, + HOMEPAGE_PATH, *HIDDEN_FOLDER_PATH_LIST) + +_project_folder = Path(__file__).parent +app = Flask(__name__, + template_folder=_project_folder / "templates", + static_folder=_project_folder / "static") + + +app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER_PATH +app.config["SECRET_KEY"] = cfg.secret_key + +# console logger +app.logger.setLevel(logging.INFO) + +# file logger +logger = logging.getLogger("werkzeug") +logger.setLevel(logging.ERROR) + +web_deps = get_web_deps(cfg.local_mode, app.logger) + +# plugins +plugin_manager = PluginManager( + flask_app=app, + config=cfg, + plugins=cfg.plugins, + web_deps=web_deps) + +wrm = WikiRepoManager(flask_app=app) +cache = Cache(cfg.cache_dir) +im = ImageManager(app, cfg) + +SYSTEM_SETTINGS = { + "darktheme": False, + "listsortMTime": False, + "web_deps": web_deps, + "plugins": plugin_manager.plugins, +} + + +@app.context_processor +def inject_file_list() -> dict: + """Context processor that injects our file list into every call.""" + return {"file_list": wiki_tree(Path(cfg.wiki_directory))} + + +class PageContent: + """Holds the content of a wiki page.""" + + def __init__(self, title: str = "", content: str = ""): + self._title = title + self.content = content + self.errors = [] + + self.is_new_page = True + """set is_new_page to false we are editing a page rather than creating a new""" + + @classmethod + def load_from_request(cls: type[PC_T]) -> PC_T: + """Load the page content from the pages request form.""" + return cls(request.form["PN"], request.form["CT"]) + + @property + def _formatted(self) -> str: + if self._title[-4:] == "{id}": + return f"{self._title[:-4]}{uuid.uuid4().hex}" + return self._title + + @property + def title(self) -> str: + """The fully qualified path including any directories, but without a suffix.""" + return Path(self._formatted).with_suffix("").as_posix()\ + if self._formatted else "" + + @property + def file_name(self) -> str: + """The name, any directories are strip out, with a suffix.""" + # If the title is empty that means we are creating + # a new file and should return it empty. + if not self._formatted: + return self._formatted + t = Path(self._formatted) + # If the path doesn't have a suffix we add .md + return t.name if t.suffix != "" else t.with_suffix(".md").name + + @property + def relative_file_path(self) -> Path: + """The relative path to the file. Excluding 'wiki' directory. + + This should just be the title but with a suffix. + """ + p = Path(self._formatted) + return p if p.suffix != "" else p.with_suffix(".md") + + @property + def file_path(self) -> Path: + """The path to the file. This will include the 'wiki' directory.""" + p = Path(safe_join(cfg.wiki_directory, self._formatted)) + return p if p.suffix != "" else p.with_suffix(".md") + + def validate(self) -> bool: + """Validate the page name, add errors to the error list for later retrival.""" + can_create_page = self.is_new_page is True and self.file_path.exists() + safe_name = "/".join([secure_filename(part) for part in self.title.split("/")]) + filename_is_ok = safe_name == self.title + if not can_create_page and filename_is_ok and self.title: # Early exist + return True + + if can_create_page: + self.errors.append("A page with that name already exists. " + "The page name needs to be unique.") + + if not filename_is_ok: + self.errors.append(f"Page name not accepted. Try using '{safe_name}'.") + + if not self.title: + self.errors.append("Your page needs a name.") + return False + + +def process(page: PageContent) -> str: + """Process the content with the plugins. + + It also manages CRLF to LF conversion. + """ + # Convert Win line ending (CRLF) to standard Unix (LF) + processed = page.content.replace("\r\n", "\n") + + # Process the content with the plugins + return plugin_manager.broadcast("process_md", processed) + + +def save(page: PageContent) -> None: + """Get file content from the form and save it.""" + app.logger.info("Saving >>> '%s' ...", page.title) + + try: + page.file_path.parent.mkdir(exist_ok=True) + + with page.file_path.open("w", encoding="utf-8") as f: + f.write(page.content) + except Exception as e: + # TODO: Use Flask Abort? + app.logger.exception("Error while saving '%s'", page.title) + + +def search(search_term: str, page: int) -> str: + """Preform a search for a term and shows the results.""" + app.logger.info("Searching >>> '%s' ...", search_term) + search_index = Search(cfg.search_dir) + page = int(page) + search_result = search_index.search(search_term, page) + results, num_results, num_pages, suggestions = search_result + return render_template( + "search.html", + search_term=search_term, + num_results=num_results, + num_pages=num_pages, + current_page=page, + suggestions=suggestions, + results=results, + system=SYSTEM_SETTINGS, + ) + + +def wiki_tree(path: Path) -> dict: + """Build a dictionary structure from the passed in folder.""" + try: + p_url = path.relative_to(cfg.wiki_directory).with_suffix("") + except ValueError: + p_url = Path() + tree = { + "name": path.stem, + "children": [], + "url": p_url.as_posix(), + "parts": len(p_url.parts), + "id": hash(p_url), + "item_mtime": path.stat().st_mtime, + } + + for name in path.iterdir(): + fn = name.as_posix() + if fn.startswith(HIDDEN_PATHS): # skip hidden paths + continue + fn = Path(fn) + if fn.is_dir(): + tree["children"].append(wiki_tree(fn)) + else: + url = fn.relative_to(cfg.wiki_directory) + tree["children"].append({ + "name": name.stem, + "url": url.as_posix(), + "parts": len(url.parts), + "id": hash(url), + "item_mtime": path.stat().st_mtime, + }) + return tree + + +def sort_tree_children(dictionary: dict, sort_by: str) -> dict: + """Reorders the dictionary and its children.""" + children = sorted(dictionary.get("children"), key=lambda item: item[sort_by]) + dictionary["children"] = children + for child in children: + if "children" in child: + child["children"] = sort_tree_children(child, sort_by) + return dictionary + + +def get_html(page: PageContent) -> [str, str]: + """Get the content of the file.""" + mod = "Last modified: %s" % time.ctime(page.file_path.stat().st_mtime) + + cached_entry = cache.get(page.file_path.as_posix()) + if cached_entry: + app.logger.info("Showing HTML page from cache >>> '%s'", page.file_name) + cached_entry = plugin_manager.broadcast("process_html", cached_entry) + return cached_entry, mod + + app.logger.info("Converting to HTML with pandoc >>> '%s' ...", page.file_name) + + if page.file_path.suffix == ".md": + html = pypandoc.convert_file( + page.file_path, + "html5", + format="md", + extra_args=["--mathjax"], + filters=["pandoc-xnos"], + ) + else: + # If the page isn't an .md page load it without + # running it through the converter. + with page.file_path.open("r", encoding="utf-8", errors="ignore") as f: + html = f.read() + + if html.strip(): + html = clean_html(html) + + html = plugin_manager.broadcast("process_before_cache_html", html) + + cache.set(page.file_path.as_posix(), html) + + html = plugin_manager.broadcast("process_html", html) + + app.logger.info("Showing HTML page >>> '%s'", page.file_name) + + return html, mod + + +@app.get("/list/") +def list_full_wiki() -> str: + """Get files in the wiki root.""" + return list_wiki("") + + +@app.get("/list//") +def list_wiki(folderpath: str) -> str: + """List all the pages in a given folder of the wiki.""" + requested_path = safe_join(cfg.wiki_directory, folderpath) + if requested_path is None: + app.logger.info("Requested unsafe path >>> showing homepage") + return index() + app.logger.info("Showing >>> all files in %s", folderpath) + requested_path = Path(requested_path) + + file_list = wiki_tree(requested_path) + # Sorting + if SYSTEM_SETTINGS["listsortMTime"]: + file_list = sort_tree_children(file_list, "listsortMTime") + else: + file_list = sort_tree_children(file_list, "url") + + return render_template( + "list_files.html", + list=file_list, + folder=folderpath, + system=SYSTEM_SETTINGS) + + +@app.get("/search") +def search_route() -> str | Response: + """Route to get result from a search.""" + if request.args.get("q"): + return search(request.args.get("q"), request.args.get("page", 1)) + flash("You didn't enter anything to search for") + return redirect("/") + + +@app.get("/") +def wiki_page(file_page: str) -> None | str | Response: + """Get wiki page.""" + git_sync_thread = Thread(target=wrm.git_pull, args=()) + git_sync_thread.start() + + if "favicon" in file_page: # if the GET request is not for the favicon + return None + + page = PageContent(title=file_page) + try: + html_content, mod = get_html(page) + except FileNotFoundError as e: + app.logger.info(e) + return redirect("/add_new?page=" + file_page) + + page.content = html_content + return render_template( + "content.html", + form=page, + folder="", + info=html_content, + modif=mod, + system=SYSTEM_SETTINGS, + ) + + +@app.get("/") +def index() -> None | str | Response: + """Render home page.""" + app.logger.info("Showing HTML page >>> 'homepage'") + + md_file_path = Path(cfg.wiki_directory) / cfg.homepage + cached_entry = cache.get(md_file_path.as_posix()) + if cached_entry: + page = PageContent(cfg.homepage_title, cached_entry) + app.logger.info("Showing HTML page from cache >>> 'homepage'") + return render_template( + "index.html", + form=page, + system=SYSTEM_SETTINGS, + ) + + try: + app.logger.info("Converting to HTML with pandoc >>> 'homepage' ...") + html = pypandoc.convert_file( + md_file_path, "html5", format="md", extra_args=["--mathjax"], + filters=["pandoc-xnos"]) + html = clean_html(html) + cache.set(md_file_path.as_posix(), html) + + except Exception as e: + # TODO: Use Flask Abort? + app.logger.exception("Conversion to HTML failed") + + page = PageContent(cfg.homepage_title, html) + return render_template("index.html", form=page, system=SYSTEM_SETTINGS) + + +@app.get("/add_new") +def add_new_view() -> str | Response: + """Add a new page.""" + if (bool(cfg.protect_edit_by_password) and + (request.cookies.get("session_wikmd") not in SESSIONS)): + return login("/add_new") + + page = PageContent(request.args.get("page") or "", "") + return render_template( + "new.html", + upload_path=cfg.images_route, + image_allowed_mime=cfg.image_allowed_mime, + form=page, + system=SYSTEM_SETTINGS, + ) + + +@app.post("/add_new") +def add_new_post() -> str | Response: + """Add a new page.""" + if (bool(cfg.protect_edit_by_password) and + (request.cookies.get("session_wikmd") not in SESSIONS)): + return login("/add_new") + page = PageContent.load_from_request() + + page.is_new_page = True + if not page.validate(): + return render_template("new.html", + form=page, + upload_path=cfg.images_route, + image_allowed_mime=cfg.image_allowed_mime, + system=SYSTEM_SETTINGS) + + save(page) + git_sync_thread = Thread(target=wrm.git_sync, args=(page.title, "Add")) + git_sync_thread.start() + + return redirect(url_for("wiki_page", file_page=page.relative_file_path)) + + +@app.get("/edit/homepage") +def edit_homepage_view() -> str | Response: + """Get the edit home page view.""" + if (bool(cfg.protect_edit_by_password) and + (request.cookies.get("session_wikmd") not in SESSIONS)): + return login("edit/homepage") + + with (Path(cfg.wiki_directory) / cfg.homepage).open("r", + encoding="utf-8", + errors="ignore") as f: + str_content = f.read() + page = PageContent(cfg.homepage_title, str_content) + return render_template( + "new.html", + form=page, + upload_path=cfg.images_route, + image_allowed_mime=cfg.image_allowed_mime, + system=SYSTEM_SETTINGS, + ) + + +@app.post("/edit/homepage") +def edit_homepage_post() -> str | Response: + """Change home page content.""" + if (bool(cfg.protect_edit_by_password) and + (request.cookies.get("session_wikmd") not in SESSIONS)): + return login("edit/homepage") + + page = PageContent.load_from_request() + page.is_new_page = False + if not page.validate(): + return render_template( + "new.html", + form=page, + upload_path=cfg.images_route, + image_allowed_mime=cfg.image_allowed_mime, + system=SYSTEM_SETTINGS, + ) + + save(page) + git_sync_thread = Thread(target=wrm.git_sync, args=(page.title, "Edit")) + git_sync_thread.start() + + return redirect(url_for("wiki_page", file_page=page.relative_file_path)) + + +@app.get("/remove/") +def remove(page: str) -> Response: # TODO: This shouldn't be a GET + """Remove a page.""" + if (bool(cfg.protect_edit_by_password) and + (request.cookies.get("session_wikmd") not in SESSIONS)): + return redirect(url_for("wiki_page", file_page=page)) + + page = PageContent(title=page) + page.file_path.unlink() + + if not any(page.file_path.parent.iterdir()): + page.file_path.parent.rmdir() + git_sync_thread = Thread(target=wrm.git_sync, args=(page, "Remove")) + git_sync_thread.start() + return redirect("/") + + +@app.get("/edit/") +def edit_view(page_name: str) -> Response | str: + """View the edit page populated with current content.""" + if (bool(cfg.protect_edit_by_password) and + (request.cookies.get("session_wikmd") not in SESSIONS)): + return login("edit/" + page_name) + + page = PageContent(page_name, "") + if page.file_path.exists(): + with page.file_path.open("r", encoding="utf-8", errors="ignore") as f: + page.content = f.read() + + return render_template( + "new.html", + form=page, + upload_path=cfg.images_route, + image_allowed_mime=cfg.image_allowed_mime, + system=SYSTEM_SETTINGS, + ) + + +@app.post("/edit/") +def edit(page_name: str) -> Response | str: + """Change page content.""" + if (bool(cfg.protect_edit_by_password) and + (request.cookies.get("session_wikmd") not in SESSIONS)): + return login("edit/" + page_name) + + page = PageContent.load_from_request() + page.is_new_page = False + if not page.validate(): + return render_template( + "new.html", + form=page, + upload_path=cfg.images_route, + image_allowed_mime=cfg.image_allowed_mime, + system=SYSTEM_SETTINGS, + ) + + if page.title != page_name: + (Path(cfg.wiki_directory) / page_name).unlink() + + save(page) + git_sync_thread = Thread(target=wrm.git_sync, args=(page.title, "Edit")) + git_sync_thread.start() + + return redirect(url_for("wiki_page", file_page=page.relative_file_path)) + + +@app.post(f"/{cfg.images_route}") +def upload_file() -> str: + """Upload file to the wiki.""" + if (bool(cfg.protect_edit_by_password) and + (request.cookies.get("session_wikmd") not in SESSIONS)): + return login() + + app.logger.info("Uploading new image ...") + return im.save_images(request.files) + + +@app.delete(f"/{cfg.images_route}") +def delete_file() -> str: + """Delete file from the wiki.""" + if (bool(cfg.protect_edit_by_password) and + (request.cookies.get("session_wikmd") not in SESSIONS)): + return login() + + # request data is in format "b'nameoffile.png" decode to utf-8 + file_name = request.data.decode("utf-8") + im.delete_image(file_name) + return "OK" + + +@app.post("/plug_com") +def communicate_plugins() -> None | Response: + """Send the request to the plugins.""" + if (bool(cfg.protect_edit_by_password) and + (request.cookies.get("session_wikmd") not in SESSIONS)): + return login() + plugin_manager.broadcast("communicate_plugin", request) + return None + + +@app.get("/knowledge-graph") +def graph() -> str: + """Get the knowledge-graph.""" + global links + links = knowledge_graph.find_links() + return render_template("knowledge-graph.html", links=links, system=SYSTEM_SETTINGS) + + +@app.get("/login") +def login_view() -> str | Response: + """Get login view.""" + app.logger.info("Display login page") + return render_template("login.html", system=SYSTEM_SETTINGS) + + +@app.post("/login") +def login(page: str) -> None | str | Response: + """Login Route.""" + password = request.form["password"] + sha_string = sha256(password.encode("utf-8")).hexdigest() + if sha_string == cfg.password_in_sha_256.lower(): + app.logger.info("User successfully logged in") + resp = make_response(redirect("/" + page)) + session = secrets.token_urlsafe(1024 // 8) + resp.set_cookie("session_wikmd", session) + SESSIONS.append(session) + return resp + app.logger.info("Login failed!") + return None + + +@app.get("/nav//") +def nav_id_to_page(id_: str) -> Response: + """Translate id to page path.""" + for i in links: + if i["id"] == int(id_): + return redirect("/" + i["path"]) + return redirect("/") + + +@app.get(f"/{cfg.images_route}/") +def display_image(image_name: str) -> str | Response: + """Get the image path route.""" + image_path = (Path(UPLOAD_FOLDER_PATH) / image_name).resolve().as_posix() + try: + response = send_file(Path(image_path).resolve()) + except Exception: + # TODO: Use Flask Abort(404)? + app.logger.exception("Could not find image: %s", image_path) + return "" + + app.logger.info("Showing image >>> '%s'", image_path) + # cache indefinitely + response.headers["Cache-Control"] = "max-age=31536000, immutable" + return response + + +@app.get("/toggle-darktheme/") +def toggle_darktheme() -> Response: + """Toggle dark theme.""" + SYSTEM_SETTINGS["darktheme"] = not SYSTEM_SETTINGS["darktheme"] + return redirect(request.args.get("return", "/")) # redirect to the same page URL + + +@app.get("/toggle-sorting/") +def toggle_sort() -> Response: + """Toggle sort mode.""" + SYSTEM_SETTINGS["listsortMTime"] = not SYSTEM_SETTINGS["listsortMTime"] + return redirect("/list") + + +@app.get("/favicon.ico") +def favicon() -> Response: + """Favicon.""" + return send_from_directory(Path(app.root_path) / "static", + "favicon.ico", mimetype="image/vnd.microsoft.icon") + + +def setup_search() -> None: + """Set up search index.""" + search_index = Search(cfg.search_dir, create=True) + + app.logger.info("Search index creation...") + items = [] + for root, _, files in os.walk(cfg.wiki_directory): + for item in files: + if ( + root.startswith((f"{cfg.wiki_directory}/.git", + f"{cfg.wiki_directory}/{cfg.images_route}")) + ): + continue + item_ = Path(item) + page_name, ext = item_.stem, item_.suffix + if ext.lower() != ".md": + continue + path = os.path.relpath(root, cfg.wiki_directory) + items.append((item, page_name, path)) + + search_index.index_all(cfg.wiki_directory, items) + + +def setup_wiki_template() -> bool: + """Copy wiki_template files into the wiki directory if it's empty.""" + root = Path(__file__).parent + + if not Path(cfg.wiki_directory).exists(): + app.logger.info("Wiki directory doesn't exists, copy template") + shutil.copytree(root / "wiki_template", cfg.wiki_directory) + return True + if not any(Path(cfg.wiki_directory).iterdir()): + app.logger.info("Wiki directory is empty, copy template") + shutil.copytree(root / "wiki_template", cfg.wiki_directory, dirs_exist_ok=True) + return True + return False + + +def run_wiki() -> None: + """Run the wiki as a Flask app.""" + app.logger.info("Starting Wikmd with wiki directory %s", + Path(cfg.wiki_directory).resolve()) + + plugin_manager.broadcast("request_html", get_html) + + if int(cfg.wikmd_logging) == 1: + logging.basicConfig(filename=cfg.wikmd_logging_file, level=logging.INFO) + + setup_wiki_template() + + upload_folder = Path(UPLOAD_FOLDER_PATH) + upload_folder.mkdir(exist_ok=True) + + wrm.initialize() + im.cleanup_images() + setup_search() + app.logger.info("Spawning search indexer watchdog") + watchdog = Watchdog(cfg.wiki_directory, cfg.search_dir) + watchdog.start() + app.run( + host=cfg.wikmd_host, + port=cfg.wikmd_port, + debug=True, + use_reloader=False, + ) + + +if __name__ == "__main__": + run_wiki() diff --git a/wiki/Features.md b/src/wikmd/wiki_template/Features.md similarity index 94% rename from wiki/Features.md rename to src/wikmd/wiki_template/Features.md index fde4b21..47c389c 100644 --- a/wiki/Features.md +++ b/src/wikmd/wiki_template/Features.md @@ -1,109 +1,109 @@ ---- -title: Features -author: Linbreux ---- - - -# Footnotes -``` -Here is a footnote reference,[^1] and another. - -[^1]: Here is the footnote. -``` -Here is a footnote reference,[^1] and another. - -[^1]: Here is the footnote. - -``` -Here is an inline note.^[Inlines notes are easier to write, since -you don't have to pick an identifier and move down to type the -note.] -``` - -Here is an inline note.^[Inlines notes are easier to write, since -you don't have to pick an identifier and move down to type the -note.] - -``` -(@good) This is a good example. - -As (@good) illustrates, ... - -``` - -(@good) This is a good example. - -As (@good) illustrates, ... - -# Split lists - -``` -1. one -2. two -3. three - - - -1. uno -2. dos -3. tres -``` -1. one -2. two -3. three - - - - -1. uno -2. dos -3. tres - - -## Change image size -``` -![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} -``` -![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} - -## References - -For references we use [pandoc-xnos](https://github.com/tomduck/pandoc-xnos) - -### Images - -``` -![This is a landscape](https://i.ibb.co/Dzp0SfC/download.jpg){#fig:id width="50%"} - -As show in @fig:id theire is a nice landscape -``` -![This is a landscape](https://i.ibb.co/Dzp0SfC/download.jpg){#fig:landscape width="50%"} - - -As show in @fig:landscape theire is a nice landscape - - -## Math -``` -$y = mx + b$ {#eq:id} - -This is visible in @eq:id -``` -$y = mx + b$ {#eq:id} - -This is visible in @eq:id - -## Etc - -This is also possible for tables and sections. Same princip but with - -``` -{#tbl:id} (for tables) -{#sec:2} (for sections) -``` - -# Pandoc - -All default pandoc features are supported with the extend of mathjax and pandoc-xnos. -![caption](/img/3a2ce07d2109eb82f779f71748be8990.webp) +--- +title: Features +author: Linbreux +--- + + +# Footnotes +``` +Here is a footnote reference,[^1] and another. + +[^1]: Here is the footnote. +``` +Here is a footnote reference,[^1] and another. + +[^1]: Here is the footnote. + +``` +Here is an inline note.^[Inlines notes are easier to write, since +you don't have to pick an identifier and move down to type the +note.] +``` + +Here is an inline note.^[Inlines notes are easier to write, since +you don't have to pick an identifier and move down to type the +note.] + +``` +(@good) This is a good example. + +As (@good) illustrates, ... + +``` + +(@good) This is a good example. + +As (@good) illustrates, ... + +# Split lists + +``` +1. one +2. two +3. three + + + +1. uno +2. dos +3. tres +``` +1. one +2. two +3. three + + + + +1. uno +2. dos +3. tres + + +## Change image size +``` +![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} +``` +![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} + +## References + +For references we use [pandoc-xnos](https://github.com/tomduck/pandoc-xnos) + +### Images + +``` +![This is a landscape](https://i.ibb.co/Dzp0SfC/download.jpg){#fig:id width="50%"} + +As show in @fig:id theire is a nice landscape +``` +![This is a landscape](https://i.ibb.co/Dzp0SfC/download.jpg){#fig:landscape width="50%"} + + +As show in @fig:landscape theire is a nice landscape + + +## Math +``` +$y = mx + b$ {#eq:id} + +This is visible in @eq:id +``` +$y = mx + b$ {#eq:id} + +This is visible in @eq:id + +## Etc + +This is also possible for tables and sections. Same princip but with + +``` +{#tbl:id} (for tables) +{#sec:2} (for sections) +``` + +# Pandoc + +All default pandoc features are supported with the extend of mathjax and pandoc-xnos. +![caption](/img/3a2ce07d2109eb82f779f71748be8990.webp) ![caption](/img/pixil-frame-07165101.png) \ No newline at end of file diff --git a/wiki/How to use the wiki.md b/src/wikmd/wiki_template/How to use the wiki.md similarity index 96% rename from wiki/How to use the wiki.md rename to src/wikmd/wiki_template/How to use the wiki.md index 3d531ee..f911c4e 100644 --- a/wiki/How to use the wiki.md +++ b/src/wikmd/wiki_template/How to use the wiki.md @@ -1,94 +1,94 @@ -## Homepage - -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. - -## Plugins - -The plugins are used to extend the functionality of the wiki. Most of them are accessible through the use of `tags`. -For now there are only a few supported. - -- `[[draw]]` Allows you to add an **interactive drawio drawing** to the wiki. -- `[[info]]`, `[[warning]]`, `[[danger]]`, `[[success]]` Adds a nice **alert message**. -- `[[ page: some-page ]]` Allows to show an other page in the current one. - -[[success]] You are ready to go! - -## Latex - -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. - -### Change image size -``` -![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} -``` -![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} - -### Image references -``` -![\label{test}](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} - -Inside picture \ref{landscape picture} you can see a nice mountain. - -``` -![picture \label{landscape picture}](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} - -Clickable reference in picture \ref{landscape picture}. - -### Math -``` -\begin{align} -y(x) &= \int_0^\infty x^{2n} e^{-a x^2}\,dx\\ -&= \frac{2n-1}{2a} \int_0^\infty x^{2(n-1)} e^{-a x^2}\,dx\\ -&= \frac{(2n-1)!!}{2^{n+1}} \sqrt{\frac{\pi}{a^{2n+1}}}\\ -&= \frac{(2n)!}{n! 2^{2n+1}} \sqrt{\frac{\pi}{a^{2n+1}}} -\end{align} -``` -\begin{align} -y(x) &= \int_0^\infty x^{2n} e^{-a x^2}\,dx\\ -&= \frac{2n-1}{2a} \int_0^\infty x^{2(n-1)} e^{-a x^2}\,dx\\ -&= \frac{(2n-1)!!}{2^{n+1}} \sqrt{\frac{\pi}{a^{2n+1}}}\\ -&= \frac{(2n)!}{n! 2^{2n+1}} \sqrt{\frac{\pi}{a^{2n+1}}} -\end{align} - -``` -You can also use $inline$ math to show $a=2$ and $b=8$ -``` -You can also use $inline$ math to show $a=2$ and $b=8$ - -And many other latex functions. - -## Converting the files - -Open the wiki folder of your instance. - -|- static -|- templates -|- **wiki** $\leftarrow$ This folder -|- wiki.py - -In this folder all the markdownfiles are listed. Editing the files will be visible in the web-version. - -|- homepage.md -|- How to use the wiki.md -|- Markdown cheatsheet.md - -The advantage is that u can use the commandline to process some data. For example using pandoc: -``` -$ pandoc -f markdown -t latex homepage.md How\ to\ use\ the\ wiki.md -o file.pdf --pdf-engine=xelatex -``` -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 -``` ---- -title: titlepage -author: your name -date: 05-11-2020 -geometry: margin=2.5cm -header-includes: | - \usepackage{caption} - \usepackage{subcaption} -lof: true ---- -``` -For more information you have to read the pandoc documentation. - -[Using the version control system](/Using the version control system) +## Homepage + +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. + +## Plugins + +The plugins are used to extend the functionality of the wiki. Most of them are accessible through the use of `tags`. +For now there are only a few supported. + +- `[[draw]]` Allows you to add an **interactive drawio drawing** to the wiki. +- `[[info]]`, `[[warning]]`, `[[danger]]`, `[[success]]` Adds a nice **alert message**. +- `[[ page: some-page ]]` Allows to show an other page in the current one. + +[[success]] You are ready to go! + +## Latex + +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. + +### Change image size +``` +![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} +``` +![](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} + +### Image references +``` +![\label{test}](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} + +Inside picture \ref{landscape picture} you can see a nice mountain. + +``` +![picture \label{landscape picture}](https://i.ibb.co/Dzp0SfC/download.jpg){width="50%"} + +Clickable reference in picture \ref{landscape picture}. + +### Math +``` +\begin{align} +y(x) &= \int_0^\infty x^{2n} e^{-a x^2}\,dx\\ +&= \frac{2n-1}{2a} \int_0^\infty x^{2(n-1)} e^{-a x^2}\,dx\\ +&= \frac{(2n-1)!!}{2^{n+1}} \sqrt{\frac{\pi}{a^{2n+1}}}\\ +&= \frac{(2n)!}{n! 2^{2n+1}} \sqrt{\frac{\pi}{a^{2n+1}}} +\end{align} +``` +\begin{align} +y(x) &= \int_0^\infty x^{2n} e^{-a x^2}\,dx\\ +&= \frac{2n-1}{2a} \int_0^\infty x^{2(n-1)} e^{-a x^2}\,dx\\ +&= \frac{(2n-1)!!}{2^{n+1}} \sqrt{\frac{\pi}{a^{2n+1}}}\\ +&= \frac{(2n)!}{n! 2^{2n+1}} \sqrt{\frac{\pi}{a^{2n+1}}} +\end{align} + +``` +You can also use $inline$ math to show $a=2$ and $b=8$ +``` +You can also use $inline$ math to show $a=2$ and $b=8$ + +And many other latex functions. + +## Converting the files + +Open the wiki folder of your instance. + +|- static +|- templates +|- **wiki** $\leftarrow$ This folder +|- wiki.py + +In this folder all the markdownfiles are listed. Editing the files will be visible in the web-version. + +|- homepage.md +|- How to use the wiki.md +|- Markdown cheatsheet.md + +The advantage is that u can use the commandline to process some data. For example using pandoc: +``` +$ pandoc -f markdown -t latex homepage.md How\ to\ use\ the\ wiki.md -o file.pdf --pdf-engine=xelatex +``` +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 +``` +--- +title: titlepage +author: your name +date: 05-11-2020 +geometry: margin=2.5cm +header-includes: | + \usepackage{caption} + \usepackage{subcaption} +lof: true +--- +``` +For more information you have to read the pandoc documentation. + +[Using the version control system](/Using the version control system) diff --git a/wiki/Markdown cheatsheet.md b/src/wikmd/wiki_template/Markdown cheatsheet.md similarity index 95% rename from wiki/Markdown cheatsheet.md rename to src/wikmd/wiki_template/Markdown cheatsheet.md index 1f576b6..a62d290 100644 --- a/wiki/Markdown cheatsheet.md +++ b/src/wikmd/wiki_template/Markdown cheatsheet.md @@ -1,243 +1,243 @@ -# Heading 1 # - - Markup : # Heading 1 # - - -OR- - - Markup : ============= (below H1 text) - -## Heading 2 ## - - Markup : ## Heading 2 ## - - -OR- - - Markup: --------------- (below H2 text) - -### Heading 3 ### - - Markup : ### Heading 3 ### - -#### Heading 4 #### - - Markup : #### Heading 4 #### - - -Common text - - Markup : Common text - -_Emphasized text_ - - Markup : _Emphasized text_ or *Emphasized text* - -~~Strikethrough text~~ - - Markup : ~~Strikethrough text~~ - -__Strong text__ - - Markup : __Strong text__ or **Strong text** - -___Strong emphasized text___ - - Markup : ___Strong emphasized text___ or ***Strong emphasized text*** - -[Named Link](http://www.google.fr/ "Named link title") and http://www.google.fr/ or - - Markup : [Named Link](http://www.google.fr/ "Named link title") and http://www.google.fr/ or - -[heading-1](#heading-1 "Goto heading-1") - - Markup: [heading-1](#heading-1 "Goto heading-1") - -Table, like this one : - -First Header | Second Header -------------- | ------------- -Content Cell | Content Cell -Content Cell | Content Cell - -``` -First Header | Second Header -------------- | ------------- -Content Cell | Content Cell -Content Cell | Content Cell -``` - -Adding a pipe `|` in a cell : - -First Header | Second Header -------------- | ------------- -Content Cell | Content Cell -Content Cell | \| - -``` -First Header | Second Header -------------- | ------------- -Content Cell | Content Cell -Content Cell | \| -``` - -Left, right and center aligned table - -Left aligned Header | Right aligned Header | Center aligned Header -| :--- | ---: | :---: -Content Cell | Content Cell | Content Cell -Content Cell | Content Cell | Content Cell - -``` -Left aligned Header | Right aligned Header | Center aligned Header -| :--- | ---: | :---: -Content Cell | Content Cell | Content Cell -Content Cell | Content Cell | Content Cell -``` - -`code()` - - Markup : `code()` - -```javascript - var specificLanguage_code = - { - "data": { - "lookedUpPlatform": 1, - "query": "Kasabian+Test+Transmission", - "lookedUpItem": { - "name": "Test Transmission", - "artist": "Kasabian", - "album": "Kasabian", - "picture": null, - "link": "http://open.spotify.com/track/5jhJur5n4fasblLSCOcrTp" - } - } - } -``` - - Markup : ```javascript - ``` - -* Bullet list - * Nested bullet - * Sub-nested bullet etc -* Bullet list item 2 - -~~~ - Markup : * Bullet list - * Nested bullet - * Sub-nested bullet etc - * Bullet list item 2 - --OR- - - Markup : - Bullet list - - Nested bullet - - Sub-nested bullet etc - - Bullet list item 2 -~~~ - -1. A numbered list - 1. A nested numbered list - 2. Which is numbered -2. Which is numbered - -~~~ - Markup : 1. A numbered list - 1. A nested numbered list - 2. Which is numbered - 2. Which is numbered -~~~ - -- [ ] An uncompleted task -- [x] A completed task - -~~~ - Markup : - [ ] An uncompleted task - - [x] A completed task -~~~ - -- [ ] An uncompleted task - - [ ] A subtask - -~~~ - Markup : - [ ] An uncompleted task - - [ ] A subtask -~~~ - -> Blockquote ->> Nested blockquote - - Markup : > Blockquote - >> Nested Blockquote - -_Horizontal line :_ -- - - - - - Markup : - - - - - -_Image with alt :_ - -![picture alt](http://via.placeholder.com/200x150 "Title is optional") - - Markup : ![picture alt](http://via.placeholder.com/200x150 "Title is optional") - -Foldable text: - -
          - Title 1 -

          Content 1 Content 1 Content 1 Content 1 Content 1

          -
          -
          - Title 2 -

          Content 2 Content 2 Content 2 Content 2 Content 2

          -
          - - Markup :
          - Title 1 -

          Content 1 Content 1 Content 1 Content 1 Content 1

          -
          - -```html -

          HTML

          -

          Some HTML code here

          -``` - -Link to a specific part of the page: - -[Go To TOP](#TOP) - - Markup : [text goes here](#section_name) - section_title - -Hotkey: - -⌘F - -⇧⌘F - - Markup : ⌘F - -Hotkey list: - -| Key | Symbol | -| --- | --- | -| Option | ⌥ | -| Control | ⌃ | -| Command | ⌘ | -| Shift | ⇧ | -| Caps Lock | ⇪ | -| Tab | ⇥ | -| Esc | ⎋ | -| Power | ⌽ | -| Return | ↩ | -| Delete | ⌫ | -| Up | ↑ | -| Down | ↓ | -| Left | ← | -| Right | → | - -Emoji: - -:exclamation: Use emoji icons to enhance text. :+1: Look up emoji codes at [emoji-cheat-sheet.com](http://emoji-cheat-sheet.com/) - +# Heading 1 # + + Markup : # Heading 1 # + + -OR- + + Markup : ============= (below H1 text) + +## Heading 2 ## + + Markup : ## Heading 2 ## + + -OR- + + Markup: --------------- (below H2 text) + +### Heading 3 ### + + Markup : ### Heading 3 ### + +#### Heading 4 #### + + Markup : #### Heading 4 #### + + +Common text + + Markup : Common text + +_Emphasized text_ + + Markup : _Emphasized text_ or *Emphasized text* + +~~Strikethrough text~~ + + Markup : ~~Strikethrough text~~ + +__Strong text__ + + Markup : __Strong text__ or **Strong text** + +___Strong emphasized text___ + + Markup : ___Strong emphasized text___ or ***Strong emphasized text*** + +[Named Link](http://www.google.fr/ "Named link title") and http://www.google.fr/ or + + Markup : [Named Link](http://www.google.fr/ "Named link title") and http://www.google.fr/ or + +[heading-1](#heading-1 "Goto heading-1") + + Markup: [heading-1](#heading-1 "Goto heading-1") + +Table, like this one : + +First Header | Second Header +------------- | ------------- +Content Cell | Content Cell +Content Cell | Content Cell + +``` +First Header | Second Header +------------- | ------------- +Content Cell | Content Cell +Content Cell | Content Cell +``` + +Adding a pipe `|` in a cell : + +First Header | Second Header +------------- | ------------- +Content Cell | Content Cell +Content Cell | \| + +``` +First Header | Second Header +------------- | ------------- +Content Cell | Content Cell +Content Cell | \| +``` + +Left, right and center aligned table + +Left aligned Header | Right aligned Header | Center aligned Header +| :--- | ---: | :---: +Content Cell | Content Cell | Content Cell +Content Cell | Content Cell | Content Cell + +``` +Left aligned Header | Right aligned Header | Center aligned Header +| :--- | ---: | :---: +Content Cell | Content Cell | Content Cell +Content Cell | Content Cell | Content Cell +``` + +`code()` + + Markup : `code()` + +```javascript + var specificLanguage_code = + { + "data": { + "lookedUpPlatform": 1, + "query": "Kasabian+Test+Transmission", + "lookedUpItem": { + "name": "Test Transmission", + "artist": "Kasabian", + "album": "Kasabian", + "picture": null, + "link": "http://open.spotify.com/track/5jhJur5n4fasblLSCOcrTp" + } + } + } +``` + + Markup : ```javascript + ``` + +* Bullet list + * Nested bullet + * Sub-nested bullet etc +* Bullet list item 2 + +~~~ + Markup : * Bullet list + * Nested bullet + * Sub-nested bullet etc + * Bullet list item 2 + +-OR- + + Markup : - Bullet list + - Nested bullet + - Sub-nested bullet etc + - Bullet list item 2 +~~~ + +1. A numbered list + 1. A nested numbered list + 2. Which is numbered +2. Which is numbered + +~~~ + Markup : 1. A numbered list + 1. A nested numbered list + 2. Which is numbered + 2. Which is numbered +~~~ + +- [ ] An uncompleted task +- [x] A completed task + +~~~ + Markup : - [ ] An uncompleted task + - [x] A completed task +~~~ + +- [ ] An uncompleted task + - [ ] A subtask + +~~~ + Markup : - [ ] An uncompleted task + - [ ] A subtask +~~~ + +> Blockquote +>> Nested blockquote + + Markup : > Blockquote + >> Nested Blockquote + +_Horizontal line :_ +- - - - + + Markup : - - - - + +_Image with alt :_ + +![picture alt](http://via.placeholder.com/200x150 "Title is optional") + + Markup : ![picture alt](http://via.placeholder.com/200x150 "Title is optional") + +Foldable text: + +
          + Title 1 +

          Content 1 Content 1 Content 1 Content 1 Content 1

          +
          +
          + Title 2 +

          Content 2 Content 2 Content 2 Content 2 Content 2

          +
          + + Markup :
          + Title 1 +

          Content 1 Content 1 Content 1 Content 1 Content 1

          +
          + +```html +

          HTML

          +

          Some HTML code here

          +``` + +Link to a specific part of the page: + +[Go To TOP](#TOP) + + Markup : [text goes here](#section_name) + section_title + +Hotkey: + +⌘F + +⇧⌘F + + Markup : ⌘F + +Hotkey list: + +| Key | Symbol | +| --- | --- | +| Option | ⌥ | +| Control | ⌃ | +| Command | ⌘ | +| Shift | ⇧ | +| Caps Lock | ⇪ | +| Tab | ⇥ | +| Esc | ⎋ | +| Power | ⌽ | +| Return | ↩ | +| Delete | ⌫ | +| Up | ↑ | +| Down | ↓ | +| Left | ← | +| Right | → | + +Emoji: + +:exclamation: Use emoji icons to enhance text. :+1: Look up emoji codes at [emoji-cheat-sheet.com](http://emoji-cheat-sheet.com/) + Markup : Code appears between colons :EMOJICODE: \ No newline at end of file diff --git a/wiki/Using the version control system.md b/src/wikmd/wiki_template/Using the version control system.md similarity index 96% rename from wiki/Using the version control system.md rename to src/wikmd/wiki_template/Using the version control system.md index b1f56f0..cf9bd12 100644 --- a/wiki/Using the version control system.md +++ b/src/wikmd/wiki_template/Using the version control system.md @@ -1,30 +1,30 @@ -## Git - -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. - -``` -git add . (or the specific file) -git commit -m "your message" (default date of today) -``` - -or you could just go to the homepage of the wiki, this will do all these automatic. - -## How to go to previous file? - -cd inside 'wikmd/wiki' - -Find the version you would like to revert to. - -``` -git log -p file.md -``` - -This will give you a long commit string. Copy the first part of it. (for example b4b580411b) - -Modify the file - -``` -git checkout b4b580411b -- file.md -``` - -Now reload the homepage or use [git](#git) +## Git + +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. + +``` +git add . (or the specific file) +git commit -m "your message" (default date of today) +``` + +or you could just go to the homepage of the wiki, this will do all these automatic. + +## How to go to previous file? + +cd inside 'wikmd/wiki' + +Find the version you would like to revert to. + +``` +git log -p file.md +``` + +This will give you a long commit string. Copy the first part of it. (for example b4b580411b) + +Modify the file + +``` +git checkout b4b580411b -- file.md +``` + +Now reload the homepage or use [git](#git) diff --git a/wiki/homepage.md b/src/wikmd/wiki_template/homepage.md similarity index 97% rename from wiki/homepage.md rename to src/wikmd/wiki_template/homepage.md index 27c882c..4cdea1e 100644 --- a/wiki/homepage.md +++ b/src/wikmd/wiki_template/homepage.md @@ -1,42 +1,42 @@ -## What is it? -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. - -## Why markdown? -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. -There are more pros: - -- Easy to process to other file formats -- Scalable, it reformats for the perfect display width - -## How does it work? -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. - -To view the documents in the browser, the document is converted to html. - -## Plugins (beta) - -The plugins are used to extend the functionality of the wiki. Most of them are accessible through the use of `tags`. -For now there are only a few supported. - -- `[[draw]]` Allows you to add an **interactive drawio drawing** to the wiki. -- `[[info]]`, `[[warning]]`, `[[danger]]`, `[[success]]` Adds a nice **alert message**. -- `[[ page: some-page ]]` Allows to show an other page in the current one. - -### Image support -![](https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Markdown-mark.svg/208px-Markdown-mark.svg.png) - -### Latex support - -$$x_{1,2} = \frac{-b ± \sqrt{b^2 - 4 a c}}{2a}$$ - -## How to use the wiki -You can learn more on how to use the wiki [here](How to use the wiki) - -## Features -Read all the features [here](Features) - -## Handy links -- [Google](http://google.be) -- [Duckduckgo](http://duckduckgo.org) - - +## What is it? +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. + +## Why markdown? +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. +There are more pros: + +- Easy to process to other file formats +- Scalable, it reformats for the perfect display width + +## How does it work? +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. + +To view the documents in the browser, the document is converted to html. + +## Plugins (beta) + +The plugins are used to extend the functionality of the wiki. Most of them are accessible through the use of `tags`. +For now there are only a few supported. + +- `[[draw]]` Allows you to add an **interactive drawio drawing** to the wiki. +- `[[info]]`, `[[warning]]`, `[[danger]]`, `[[success]]` Adds a nice **alert message**. +- `[[ page: some-page ]]` Allows to show an other page in the current one. + +### Image support +![](https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Markdown-mark.svg/208px-Markdown-mark.svg.png) + +### Latex support + +$$x_{1,2} = \frac{-b ± \sqrt{b^2 - 4 a c}}{2a}$$ + +## How to use the wiki +You can learn more on how to use the wiki [here](How to use the wiki) + +## Features +Read all the features [here](Features) + +## Handy links +- [Google](http://google.be) +- [Duckduckgo](http://duckduckgo.org) + + diff --git a/wikmd-config.yaml b/src/wikmd/wikmd-config.yaml similarity index 100% rename from wikmd-config.yaml rename to src/wikmd/wikmd-config.yaml diff --git a/static/css/wiki.css b/static/css/wiki.css deleted file mode 100644 index ea70b86..0000000 --- a/static/css/wiki.css +++ /dev/null @@ -1,133 +0,0 @@ -@import "wiki.colors.css"; - -@import url('https://fonts.googleapis.com/css2?family=Gothic+A1:wght@100;200;300;400;500;600;700;800;900&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@100;200;300;400;500;600;700;800;900&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;200;300;400;500;600;700;800;900&display=swap'); - -body { - background-color: var(--bg-light); - color: var(--txt-light); -} - -.modal-content { - background-color: var(--bg-modal-light); - color: var(--txt-light); -} - -blockquote { - padding: 5px 50px; - margin-left: 0px; - border-left: 4px solid var(--quotes-color); - background-color: var(--bg-codeblock-light); -} - -blockquote p{ - margin-top:10px; - font-weight: 300; -} - -code, pre { - font-family: monospace; - background-color: var(--bg-codeblock-light); - color: #800; - border-radius: 5px; - padding: 4px; - font-size: 14px -} - -pre { - background-color: var(--bg-codeblock-light); - padding: 6px; - border-radius: 6px; - position: relative; -} - -pre img { - position: absolute; - top: 5px; - right: 5px; - margin-top: 5px; - margin-right: 5px; - padding: 0.15rem; - cursor: pointer; - display: inline; -} - -img { - max-width: 100% -} - -figure { - text-align: center -} - -.container-fluid { - padding-right: 0; - padding-left: 0; -} - -.hljs { - background-color: var(--bg-codeblock-light); -} - - -table { - border-collapse: collapse; - width: 100%; - margin: 10px 0px; -} - -th { - padding-top: 12px; - padding-bottom: 12px; - text-align: left; - border-bottom: 2px solid #dee2e6; -} - -td { - padding: 12px; - border-top: 1px solid #dee2e6; -} - -h1{ - font-size: 2em; - font-family: "quicksand"; -} - -h1.title{ - font-size: 2.5em; - font-family: "Helvetica"; -} - -h2 { - font-size: 1.8em; - font-family: "quicksand"; -} - -h3 { - font-size: 1.5em; - font-family: "quicksand"; -} - - -h4{ - font-size: 1.3em; - font-family: "quicksand"; -} - - -.alert i[class^="bi-"]{ - font-size: 1.5em; - line-height: 1; - margin-right: 10px; - } - -div.html-integration{ - background-color: var(--html-integration-background-light); - padding: 20px; -} - - -.mermaid { - text-align: center; -} diff --git a/templates/content.html b/templates/content.html deleted file mode 100644 index a564b1e..0000000 --- a/templates/content.html +++ /dev/null @@ -1,70 +0,0 @@ -{% extends 'base.html' %} - -{% block head %} - - -{% endblock %} - -{% block content %} -

          {% if folder %} - {{ folder }}/ - {% endif %} - {{ title }} - - - - - - - -

          - {{ info|safe }} -
          -

          {{ modif }}

          -
          - -
          - - - - -{% endblock %} - -{% block scripts %} - - -{% endblock %} diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 0f64059..0000000 --- a/templates/index.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'base.html'%} - -{%block content%} - - - - - -{{homepage|safe}} - -{%endblock%} diff --git a/tests/test_basics.py b/tests/test_basics.py index 0f4b371..a0ca4ef 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,147 +1,201 @@ -import wiki -import pytest -import pypandoc import os +from pathlib import Path +import shutil + +import pytest +from wikmd import wiki +from wikmd.wiki import app, cfg, setup_wiki_template + + +@pytest.fixture(scope="function", autouse=True) +def wiki_path(tmp_path: Path): + """Sets up the temporary wiki path. + autouse=True is needed as this behaves as a setup for the tests. + """ + wiki_path = tmp_path / "wiki" + wiki_path.mkdir() + cfg.wiki_directory = wiki_path.as_posix() + setup_wiki_template() + return wiki_path -from wiki import app -@pytest.fixture +@pytest.fixture() def client(): return app.test_client -# test if homepage responses + +@pytest.fixture(scope="module") +def test_file_content(): + return b"this is the header", b"extra content" + + +@pytest.fixture() +def project_file(wiki_path, test_file_content): + testing_folder = wiki_path / "testing_folder" + testing_folder.mkdir() + + test_file = testing_folder / "test.md" + with test_file.open("wb") as fp: + fp.writelines(test_file_content) + + yield test_file + + shutil.rmtree(str(testing_folder)) + + +@pytest.fixture() +def wiki_file(project_file): + return f"/{project_file.parent.name}/{project_file.stem}" + + def test_homepage(): + """Homepage renders.""" rv = app.test_client().get("/") - - # see if hompage responses + assert rv.status_code == 200 # Check if homepage loads - assert b'What is it?' in rv.data + assert b"What is it?" in rv.data + -# check if list returns all files (tests only 2 defaults) def test_list(): + """List functionality returns one of the standard files.""" rv = app.test_client().get("/list/") assert rv.status_code == 200 - #assert b'homepage.md' in rv.data - assert b'Features.md' in rv.data + assert b"Features.md" in rv.data -# creates a file and check if the content of the file is visible in the wiki -def test_create_file_in_folder(): - # create dir if it does not exist - if not os.path.exists("wiki/testing_folder_0123"): - os.makedirs("wiki/testing_folder_0123") - # write content in the test.md file - f = open("wiki/testing_folder_0123/test.md","w+") - f.write("# this is the header\n extra content") - f.close() - - rv = app.test_client().get("/testing_folder_0123/test") +def test_create_file_in_folder(wiki_file, test_file_content): + """Test for accessing file that exists, GET '/{file_name}'.""" + rv = app.test_client().get(wiki_file) assert rv.status_code == 200 - assert b'this is the header' in rv.data - assert b'extra content' in rv.data - - # remove created folders - os.remove("wiki/testing_folder_0123/test.md") - os.removedirs("wiki/testing_folder_0123/") - -# checks if the search response with searchterm = Features + assert test_file_content[0] in rv.data + assert test_file_content[1] in rv.data + + def test_search(): - wiki.setup_search() - rv = app.test_client().get("/?q=Features") - assert rv.status_code == 200 - assert b'Found' in rv.data - assert b'result(s)' in rv.data - assert b'Features' in rv.data - -# create a new file using the wiki and check if it is visible in the wiki -def test_new_file(): + """Search functionality returns result.""" + wiki.setup_search() + rv = app.test_client().get("/search?q=Features") + assert rv.status_code == 200 + assert b"Found" in rv.data + assert b"result(s)" in rv.data + assert b"Features" in rv.data + + +def test_add_new_file(wiki_path): + """App can create files.""" rv = app.test_client().get("/add_new") assert rv.status_code == 200 - assert b'content' in rv.data + assert b"content" in rv.data # create new file - rv = app.test_client().post("/add_new", data=dict( - PN="testing01234filenotexisting", - CT="#testing file\n this is a test" - )) + app.test_client().post("/add_new", data={ + "PN": "testing01234filenotexisting", + "CT": "#testing file\n this is a test", + }) # look at file rv = app.test_client().get("/testing01234filenotexisting") - assert b'testing file' in rv.data - assert b'this is a test' in rv.data + assert b"testing file" in rv.data + assert b"this is a test" in rv.data + + f = wiki_path / "testing01234filenotexisting.md" + f.unlink() + + +def test_bad_file_names(): + """Test for creating files with odd character in names.""" + # Disallowed + bad_name = "file*with*star" + bad_all_bad = '<>:"/\|?*' + + r = app.test_client().post("/add_new", data={ + "PN": bad_all_bad, + "CT": "#testing file\n this is a test", + }) + assert r.status_code == 200 + assert b"Page name not accepted." in r.data + + r = app.test_client().post("/add_new", data={ + "PN": bad_name, + "CT": "#testing file\n this is a test", + }) + assert r.status_code == 200 + assert bad_name.replace("*", "").encode() in r.data + + +def test_ok_file_names(wiki_path): + """Test for creating files with odd character in names.""" + # Disallowed + ok_name1 = "file with space" + ok_name2 = 'file with slash/is a folder' + r = app.test_client().post("/add_new", data={ + "PN": ok_name1, + "CT": "#testing file\n this is a test", + }) + assert r.status_code == 302 + assert (wiki_path / ok_name1).with_suffix(".md").exists() + + r = app.test_client().post("/add_new", data={ + "PN": ok_name2, + "CT": "#testing file\n this is a test", + }) + assert r.status_code == 302 + assert (wiki_path / ok_name2).with_suffix(".md").exists() - os.remove("wiki/testing01234filenotexisting.md") # create a new file in a folder using the wiki and check if it is visible in the wiki -def test_new_file_folder(): +def test_new_file_folder(wiki_path): + """App can create folders.""" rv = app.test_client().get("/add_new") assert rv.status_code == 200 - assert b'content' in rv.data + assert b"content" in rv.data + + # create new file in a folder + app.test_client().post("/add_new", data={ + "PN": "testingfolder01234/testing01234filenotexisting", + "CT": "#testing file\n this is a test", + }) - # create new file - rv = app.test_client().post("/add_new", data=dict( - PN="testingfolder01234/testing01234filenotexisting", - CT="#testing file\n this is a test" - )) - # look at file rv = app.test_client().get("/testingfolder01234/testing01234filenotexisting") - assert b'testing file' in rv.data - assert b'this is a test' in rv.data + assert b"testing file" in rv.data + assert b"this is a test" in rv.data + + f = wiki_path / "testingfolder01234" + shutil.rmtree(f) - os.remove("wiki/testingfolder01234/testing01234filenotexisting.md") - os.removedirs("wiki/testingfolder01234") # edits file using the wiki and check if it is visible in the wiki -def test_edit_file(): - f = open("wiki/testing01234filenotexisting.md","w+") - f.write("# this is the header\n extra content") - f.close() +def test_get_file_after_file_edit(project_file, wiki_file): + with project_file.open("w+") as fp: + fp.write("our new content") - rv = app.test_client().get("/edit/testing01234filenotexisting") + rv = app.test_client().get(wiki_file) assert rv.status_code == 200 - assert b'this is the header' in rv.data - - # create new file - rv = app.test_client().post("/edit/testing01234filenotexisting", data=dict( - PN="testing01234filenotexisting", - CT="#testing file\n this is a test" - )) - - # look at file - rv = app.test_client().get("/testing01234filenotexisting") - assert b'testing file' in rv.data - assert b'this is a test' in rv.data + assert b"our new content" in rv.data - os.remove("wiki/testing01234filenotexisting.md") -# edits file in folder using the wiki and check if it is visible in the wiki -def test_edit_file_folder(): - if not os.path.exists("wiki/testingfolder01234"): - os.makedirs("wiki/testingfolder01234") +def test_get_file_after_api_edit(wiki_file): + # Edit the file through API + app.test_client().post(f"/edit{wiki_file}", data={ + "PN": wiki_file[1:], + "CT": "#testing file\n this is a test", + }) - f = open("wiki/testingfolder01234/testing01234filenotexisting.md","w+") - f.write("# this is the header\n extra content") - f.close() + rv = app.test_client().get(wiki_file) + assert b"testing file" in rv.data + assert b"this is a test" in rv.data - rv = app.test_client().get("/edit/testingfolder01234/testing01234filenotexisting") - assert rv.status_code == 200 - assert b'this is the header' in rv.data - # create new file - rv = app.test_client().post("/edit/testingfolder01234/testing01234filenotexisting", data=dict( - PN="testingfolder01234/testing01234filenotexisting", - CT="#testing file\n this is a test" - )) - - # look at file - rv = app.test_client().get("/testingfolder01234/testing01234filenotexisting") - assert b'testing file' in rv.data - assert b'this is a test' in rv.data +# edits file in folder using the wiki and check if it is visible in the wiki +def test_get_edit_page_content(project_file, wiki_file): + with project_file.open("w+") as fp: + fp.write("# this is the header\n extra content") - os.remove("wiki/testingfolder01234/testing01234filenotexisting.md") - os.removedirs("wiki/testingfolder01234") + rv = app.test_client().get(f"/edit{wiki_file}") + assert rv.status_code == 200 + assert b"this is the header" in rv.data diff --git a/tests/test_plugins.py b/tests/test_plugins.py index acd8606..7cd7feb 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,38 +1,39 @@ -import wiki import pytest -import pypandoc -import os +from wikmd import wiki +from wikmd.wiki import app -from wiki import app -@pytest.fixture +@pytest.fixture() def client(): return app.test_client -def test_plugin_loading(): - assert wiki.plugins -def test_process_md(): +@pytest.fixture() +def plugin_manager(): + return wiki.plugin_manager + + +def test_plugin_loading(plugin_manager): + assert plugin_manager.plugins + + +def test_process_md(plugin_manager): before = "#test this is test\n text should still be available after plugin" md = before - for plugin in wiki.plugins: - if ("process_md" in dir(plugin)): - md = plugin.process_md(md) + md = plugin_manager.broadcast("process_md", md) assert md == before -def test_draw_md(): + +def test_draw_md(plugin_manager): before = "#test this is test\n[[draw]] \n next line" md = before - for plugin in wiki.plugins: - if ("process_md" in dir(plugin)): - md = plugin.process_md(md) + md = plugin_manager.broadcast("process_md", md) assert md != before assert md != "" - -def test_process_html(): + + +def test_process_html(plugin_manager): before = "

          this is a test

          " html = before - for plugin in wiki.plugins: - if ("process_html" in dir(plugin)): - html = plugin.process_html(html) - assert html == before \ No newline at end of file + html = plugin_manager.broadcast("process_html", html) + assert html == before diff --git a/tests/test_search.py b/tests/test_search.py index ba974f6..7155634 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,87 +1,95 @@ import os import tempfile import time -from unittest import mock +from pathlib import Path +import pytest -from search import Search, Watchdog -from wiki import app +from wikmd.search import Search, Watchdog + + +@pytest.fixture(scope="module") +def search_dir(): + return tempfile.mkdtemp() + + +@pytest.fixture() +def search_file(): + file_name = "test_index.md" + title = "test index" + content = "index\nsearch\ntest" + return file_name, title, content + + +@pytest.fixture() +def search_engine(search_dir): + return Search(search_dir, create=True) + + +@pytest.fixture() +def search_engine_with_content(search_engine, search_dir, search_file): + file_name, title, content = search_file + + search_engine.index(search_dir, file_name, title, content) + return search_engine def test_textify(): tmp = tempfile.mkdtemp() s = Search(tmp, create=True) - md = "\n".join(("# h1", "## h2", "test")) + md = "# h1\n## h2\ntest" assert s.textify(md) == "h1\nh2\ntest" -def test_index_and_search(): - tmp = tempfile.mkdtemp() - s = Search(tmp, create=True) - fname, title = "test_index.md", "test index" - content = "\n".join(("index", "search" "test")) - - s.index(tmp, fname, title, content) - res, total, pages, _ = s.search("index", 1) +def test_search(search_engine_with_content, search_dir, search_file): + res, total, pages, _ = search_engine_with_content.search("index", 1) assert total == 1 assert pages == 1 - assert res[0].path == tmp - assert res[0].filename == fname + assert res[0].path == search_dir + assert res[0].filename == search_file[0] - _, _, _, sug = s.search("ndex", 1) + _, _, _, sug = search_engine_with_content.search("ndex", 1) assert "index" in sug -def test_pagination(): - tmp = tempfile.mkdtemp() - s = Search(tmp, create=True) - +def test_pagination(search_engine, search_dir): for i in range(25): fname, title = f"test_index_{i}.md", f"test index {i}" - content = "\n".join(("index", "search" "test")) - s.index(tmp, fname, title, content) + content = "index\nsearch\ntest" + search_engine.index(search_dir, fname, title, content) - res, total, pages, _ = s.search("index", 1) + res, total, pages, _ = search_engine.search("index", 1) assert total == 25 assert pages == 3 -def test_index_and_delete(): - tmp = tempfile.mkdtemp() - s = Search(tmp, create=True) - fname, title = "test_index.md", "test index" - content = "\n".join(("index", "search" "test")) - - s.index(tmp, fname, title, content) - res, total, pages, _ = s.search("index", 1) - assert total == 1 - assert pages == 1 - assert res[0].filename == fname - - s.delete(tmp, fname) - res, total, pages, _ = s.search("index", 1) +def test_index_and_delete(search_engine_with_content, search_dir, search_file ): + search_engine_with_content.delete(search_dir, search_file[0]) + res, total, pages, _ = search_engine_with_content.search("index", 1) assert total == 0 assert pages == 0 assert len(res) == 0 -def test_index_all(): - tmps, tmpd = tempfile.mkdtemp(), tempfile.mkdtemp() - s = Search(tmps, create=True) +def test_index_all(search_engine, search_dir): nf = [] - content = "\n".join(("index", "search" "test")) + content = "index\nsearch\ntest" for n in ("a", "b"): - fname = f"{n}.md" - with open(os.path.join(tmpd, fname), "w") as f: + file_name = f"{n}.md" + p = Path(search_dir) / file_name + with p.open("w") as f: f.write(content) - nf.append((fname, n, ".")) + nf.append((file_name, n, ".")) - os.mkdir(os.path.join(tmpd, "z")) - with open(os.path.join(tmpd, "z", "y.md"), "w") as f: + # Add a file to a sub folder + p_dir = Path(search_dir) + (p_dir / "z").mkdir() + file_name = p_dir / "z" / "y.md" + with file_name.open("w") as f: f.write(content) nf.append(("y.md", "y", "z")) - s.index_all(tmpd, nf) - res, total, pages, _ = s.search("index", 1) + search_engine.index_all(search_dir, nf) + res, total, pages, _ = search_engine.search("index", 1) assert total == 3 assert pages == 1 assert len(res) == 3 diff --git a/utils.py b/utils.py deleted file mode 100644 index c63ed4b..0000000 --- a/utils.py +++ /dev/null @@ -1,38 +0,0 @@ -import os - - -def pathify(path1, path2): - """ - Joins two paths and eventually converts them from Win (\\) to linux OS separator. - :param path1: first path - :param path2: second path - :return safe joined path - """ - return os.path.join(path1, path2).replace("\\", "/") - - -def move_all_files(src_dir: str, dest_dir: str): - """ - Function that moves all the files from a source directory to a destination one. - If a file with the same name is already present in the destination, the source file will be renamed with a - '-copy-XX' suffix. - :param src_dir: source directory - :param dest_dir: destination directory - """ - if not os.path.isdir(dest_dir): - os.mkdir(dest_dir) # make the dir if it doesn't exist - - src_files = os.listdir(src_dir) - dest_files = os.listdir(dest_dir) - - for file in src_files: - new_file = file - copies_count = 1 - while new_file in dest_files: # if the file is already present, append '-copy-XX' to the file name - file_split = file.split('.') - new_file = f"{file_split[0]}-copy-{copies_count}" - if len(file_split) > 1: # if the file has an extension (it's not a directory nor a file without extension) - new_file += f".{file_split[1]}" # add the extension - copies_count += 1 - - os.rename(f"{src_dir}/{file}", f"{dest_dir}/{new_file}") \ No newline at end of file diff --git a/wiki.py b/wiki.py deleted file mode 100644 index b5435c3..0000000 --- a/wiki.py +++ /dev/null @@ -1,541 +0,0 @@ -import os -from os.path import exists -import time -import logging -import uuid -from lxml.html.clean import clean_html -import pypandoc -import knowledge_graph -import secrets - -from flask import Flask, render_template, request, redirect, url_for, make_response, send_file, \ - send_from_directory, flash - -from werkzeug.utils import safe_join, secure_filename -from threading import Thread -from hashlib import sha256 -from cache import Cache -from image_manager import ImageManager -from config import WikmdConfig -from git_manager import WikiRepoManager -from search import Search, Watchdog -from web_dependencies import get_web_deps -from plugins.load_plugins import PluginLoader - -from utils import pathify - -SESSIONS = [] - -cfg = WikmdConfig() - -UPLOAD_FOLDER_PATH = pathify(cfg.wiki_directory, cfg.images_route) -GIT_FOLDER_PATH = pathify(cfg.wiki_directory, '.git') -HIDDEN_FOLDER_PATH_LIST = [pathify(cfg.wiki_directory, hidden_folder) for hidden_folder in cfg.hide_folder_in_wiki] -HOMEPAGE_PATH = pathify(cfg.wiki_directory, cfg.homepage) -HIDDEN_PATHS = tuple([UPLOAD_FOLDER_PATH, GIT_FOLDER_PATH, HOMEPAGE_PATH] + HIDDEN_FOLDER_PATH_LIST) - -app = Flask(__name__) -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER_PATH -app.config['SECRET_KEY'] = cfg.secret_key - -# console logger -app.logger.setLevel(logging.INFO) - -# file logger -logger = logging.getLogger('werkzeug') -logger.setLevel(logging.ERROR) - -web_deps = get_web_deps(cfg.local_mode, app.logger) - -# plugins -plugins = PluginLoader(flask_app=app, config=cfg, plugins=cfg.plugins, web_deps=web_deps).get_plugins() - -wrm = WikiRepoManager(flask_app=app) -cache = Cache(cfg.cache_dir) -im = ImageManager(app, cfg) - -SYSTEM_SETTINGS = { - "darktheme": False, - "listsortMTime": False, - "web_deps": web_deps, - "plugins": plugins -} - -def process(content: str, page_name: str): - """ - Function that processes the content with the plugins. - It also manages CRLF to LF conversion. - :param content: content - :param page_name: name of the page - :return processed content - """ - # Convert Win line ending (CRLF) to standard Unix (LF) - processed = content.replace("\r\n", "\n") - - # Process the content with the plugins - for plugin in plugins: - if "process_md" in dir(plugin): - app.logger.info(f"Plug/{plugin.get_plugin_name()} - process_md >>> {page_name}") - processed = plugin.process_md(processed) - - return processed - - -def ensure_page_can_be_created(page, page_name): - filename = safe_join(cfg.wiki_directory, f"{page_name}.md") - path_exists = os.path.exists(filename) - safe_name = "/".join([secure_filename(part) for part in page_name.split("/")]) - filename_is_ok = safe_name == page_name - if not path_exists and filename_is_ok and page_name: # Early exist - return - - if path_exists: - flash('A page with that name already exists. The page name needs to be unique.') - app.logger.info(f"Page name exists >>> {page_name}.") - - if not filename_is_ok: - flash(f"Page name not accepted. Try using '{safe_name}'.") - app.logger.info(f"Page name isn't secure >>> {page_name}.") - - if not page_name: - flash(f"Your page needs a name.") - app.logger.info(f"No page name provided.") - - content = process(request.form['CT'], page_name) - return render_template("new.html", content=content, title=page, upload_path=cfg.images_route, - image_allowed_mime=cfg.image_allowed_mime, system=SYSTEM_SETTINGS) - - -def save(page_name): - """ - Function that processes and saves a *.md page. - :param page_name: name of the page - """ - content = process(request.form['CT'], page_name) - app.logger.info(f"Saving >>> '{page_name}' ...") - - try: - filename = safe_join(cfg.wiki_directory, f"{page_name}.md") - dirname = os.path.dirname(filename) - if not os.path.exists(dirname): - os.makedirs(dirname) - with open(filename, 'w', encoding="utf-8") as f: - f.write(content) - except Exception as e: - app.logger.error(f"Error while saving '{page_name}' >>> {str(e)}") - - -def search(search_term: str, page: int): - """ - Function that searches for a term and shows the results. - """ - app.logger.info(f"Searching >>> '{search_term}' ...") - search = Search(cfg.search_dir) - page = int(page) - results, num_results, num_pages, suggestions = search.search(search_term, page) - return render_template( - 'search.html', - search_term=search_term, - num_results=num_results, - num_pages=num_pages, - current_page=page, - suggestions=suggestions, - results=results, - system=SYSTEM_SETTINGS, - ) - - -def fetch_page_name() -> str: - page_name = request.form['PN'] - if page_name[-4:] == "{id}": - page_name = f"{page_name[:-4]}{uuid.uuid4().hex}" - return page_name - - -def get_html(file_page): - """ - Function to return the html of a certain file page - """ - md_file_path = safe_join(cfg.wiki_directory, f"{file_page}.md") - mod = "Last modified: %s" % time.ctime(os.path.getmtime(md_file_path)) - folder = file_page.split("/") - file_page = folder[-1:][0] - folder = folder[:-1] - folder = "/".join(folder) - - cached_entry = cache.get(md_file_path) - if cached_entry: - app.logger.info(f"Showing HTML page from cache >>> '{file_page}'") - - for plugin in plugins: - if "process_html" in dir(plugin): - app.logger.info(f"Plug/{plugin.get_plugin_name()} - process_html >>> {file_page}") - cached_entry = plugin.process_html(cached_entry) - - return cached_entry, mod - - app.logger.info(f"Converting to HTML with pandoc >>> '{md_file_path}' ...") - - html = pypandoc.convert_file(md_file_path, "html5", - format='md', extra_args=["--mathjax"], filters=['pandoc-xnos']) - - if html.strip(): - html = clean_html(html) - - for plugin in plugins: - if "process_before_cache_html" in dir(plugin): - app.logger.info(f"Plug/{plugin.get_plugin_name()} - process_before_cache_html >>> {file_page}") - html = plugin.process_before_cache_html(html) - - cache.set(md_file_path, html) - - for plugin in plugins: - if "process_html" in dir(plugin): - app.logger.info(f"Plug/{plugin.get_plugin_name()} - process_html >>> {file_page}") - html = plugin.process_html(html) - - app.logger.info(f"Showing HTML page >>> '{file_page}'") - - return html, mod - - -@app.route('/list/', methods=['GET']) -def list_full_wiki(): - return list_wiki("") - - -@app.route('/list//', methods=['GET']) -def list_wiki(folderpath): - """ - Lists all the pages in a given folder of the wiki. - """ - files_list = [] - - requested_path = safe_join(cfg.wiki_directory, folderpath) - if requested_path is None: - app.logger.info("Requested unsafe path >>> showing homepage") - return index() - app.logger.info(f"Showing >>> all files in {folderpath}") - - for item in os.listdir(requested_path): - item_path = pathify(requested_path, item) # wiki/dir1/dir2/... - item_mtime = os.path.getmtime(item_path) - - if not item_path.startswith(HIDDEN_PATHS): # skip hidden paths - rel_item_path = item_path[len(cfg.wiki_directory + "/"):] # dir1/dir2/... - item_url = os.path.splitext(rel_item_path)[0] # eventually drop the extension - folder = rel_item_path if os.path.isdir(item_path) else "" - - info = { - 'doc': item, - 'url': item_url, - 'folder': folder, - 'folder_url': folder, - 'mtime': item_mtime, - } - files_list.append(info) - - # Sorting - if SYSTEM_SETTINGS['listsortMTime']: - files_list.sort(key=lambda x: x["mtime"], reverse=True) - else: - files_list.sort(key=lambda x: (str(x["url"]).casefold())) - - return render_template('list_files.html', list=files_list, folder=folderpath, system=SYSTEM_SETTINGS) - - -@app.route('/', methods=['GET']) -def file_page(file_page): - if request.args.get("q"): - return search(request.args.get("q"), request.args.get("page", 1)) - else: - - git_sync_thread = Thread(target=wrm.git_pull, args=()) - git_sync_thread.start() - - html = "" - mod = "" - folder = "" - - if "favicon" in file_page: # if the GET request is not for the favicon - return - - try: - html_content, mod = get_html(file_page) - - return render_template( - 'content.html', title=file_page, folder=folder, info=html_content, modif=mod, - system=SYSTEM_SETTINGS - ) - except FileNotFoundError as e: - app.logger.info(e) - return redirect("/add_new?page=" + file_page) - - -@app.route('/', methods=['GET']) -def index(): - if request.args.get("q"): - return search(request.args.get("q"), request.args.get("page", 1)) - else: - - html = "" - app.logger.info("Showing HTML page >>> 'homepage'") - - md_file_path = os.path.join(cfg.wiki_directory, cfg.homepage) - cached_entry = cache.get(md_file_path) - if cached_entry: - app.logger.info("Showing HTML page from cache >>> 'homepage'") - return render_template( - 'index.html', homepage=cached_entry, system=SYSTEM_SETTINGS - ) - - try: - app.logger.info("Converting to HTML with pandoc >>> 'homepage' ...") - html = pypandoc.convert_file( - md_file_path, "html5", format='md', extra_args=["--mathjax"], - filters=['pandoc-xnos']) - html = clean_html(html) - cache.set(md_file_path, html) - - except Exception as e: - app.logger.error(f"Conversion to HTML failed >>> {str(e)}") - - return render_template('index.html', homepage=html, system=SYSTEM_SETTINGS) - - -@app.route('/add_new', methods=['POST', 'GET']) -def add_new(): - if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): - return login("/add_new") - if request.method == 'POST': - page_name = fetch_page_name() - - re_render_page = ensure_page_can_be_created(page_name, page_name) - if re_render_page: - return re_render_page - - save(page_name) - git_sync_thread = Thread(target=wrm.git_sync, args=(page_name, "Add")) - git_sync_thread.start() - - return redirect(url_for("file_page", file_page=page_name)) - else: - page_name = request.args.get("page") - if page_name is None: - page_name = "" - return render_template('new.html', upload_path=cfg.images_route, - image_allowed_mime=cfg.image_allowed_mime, title=page_name, system=SYSTEM_SETTINGS) - - -@app.route('/edit/homepage', methods=['POST', 'GET']) -def edit_homepage(): - if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): - return login("edit/homepage") - - if request.method == 'POST': - page_name = fetch_page_name() - - save(page_name) - git_sync_thread = Thread(target=wrm.git_sync, args=(page_name, "Edit")) - git_sync_thread.start() - - return redirect(url_for("file_page", file_page=page_name)) - else: - - with open(os.path.join(cfg.wiki_directory, cfg.homepage), 'r', encoding="utf-8", errors='ignore') as f: - - content = f.read() - return render_template("new.html", content=content, title=cfg.homepage_title, upload_path=cfg.images_route, - image_allowed_mime=cfg.image_allowed_mime, system=SYSTEM_SETTINGS) - - -@app.route('/remove/', methods=['GET']) -def remove(page): - if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): - return redirect(url_for("file_page", file_page=page)) - - filename = safe_join(cfg.wiki_directory, f"{page}.md") - os.remove(filename) - if not os.listdir(os.path.dirname(filename)): - os.removedirs(os.path.dirname(filename)) - git_sync_thread = Thread(target=wrm.git_sync, args=(page, "Remove")) - git_sync_thread.start() - return redirect("/") - - -@app.route('/edit/', methods=['POST', 'GET']) -def edit(page): - if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): - return login("edit/" + page) - - filename = safe_join(cfg.wiki_directory, f"{page}.md") - if request.method == 'POST': - page_name = fetch_page_name() - - if page_name != page: - re_render_page = ensure_page_can_be_created(page_name, page_name) - if re_render_page: - return re_render_page - - os.remove(filename) - - save(page_name) - git_sync_thread = Thread(target=wrm.git_sync, args=(page_name, "Edit")) - git_sync_thread.start() - - return redirect(url_for("file_page", file_page=page_name)) - else: - if exists(filename): - with open(filename, 'r', encoding="utf-8", errors='ignore') as f: - content = f.read() - return render_template("new.html", content=content, title=page, upload_path=cfg.images_route, - image_allowed_mime=cfg.image_allowed_mime, system=SYSTEM_SETTINGS) - else: - logger.error(f"{filename} does not exists. Creating a new one.") - return render_template("new.html", content="", title=page, upload_path=cfg.images_route, - image_allowed_mime=cfg.image_allowed_mime, system=SYSTEM_SETTINGS) - - -@app.route(os.path.join("/", cfg.images_route), methods=['POST', 'DELETE']) -def upload_file(): - if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): - return login() - app.logger.info(f"Uploading new image ...") - # Upload image when POST - if request.method == "POST": - return im.save_images(request.files) - - # DELETE when DELETE - if request.method == "DELETE": - # request data is in format "b'nameoffile.png" decode to utf-8 - file_name = request.data.decode("utf-8") - im.delete_image(file_name) - return 'OK' - - -@app.route("/plug_com", methods=['POST']) -def communicate_plugins(): - if bool(cfg.protect_edit_by_password) and (request.cookies.get('session_wikmd') not in SESSIONS): - return login() - if request.method == "POST": - for plugin in plugins: - if "communicate_plugin" in dir(plugin): - return plugin.communicate_plugin(request) - return "nothing to do" - - -@app.route('/knowledge-graph', methods=['GET']) -def graph(): - global links - links = knowledge_graph.find_links() - return render_template("knowledge-graph.html", links=links, system=SYSTEM_SETTINGS) - - -@app.route('/login', methods=['GET', 'POST']) -def login(page): - if request.method == "POST": - password = request.form["password"] - sha_string = sha256(password.encode('utf-8')).hexdigest() - if sha_string == cfg.password_in_sha_256.lower(): - app.logger.info("User successfully logged in") - resp = make_response(redirect("/" + page)) - session = secrets.token_urlsafe(1024 // 8) - resp.set_cookie("session_wikmd", session) - SESSIONS.append(session) - return resp - else: - app.logger.info("Login failed!") - else: - app.logger.info("Display login page") - return render_template("login.html", system=SYSTEM_SETTINGS) - - -# Translate id to page path - - -@app.route('/nav//', methods=['GET']) -def nav_id_to_page(id): - for i in links: - if i["id"] == int(id): - return redirect("/" + i["path"]) - return redirect("/") - - -@app.route(os.path.join("/", cfg.images_route, "")) -def display_image(image_name): - image_path = safe_join(UPLOAD_FOLDER_PATH, image_name) - try: - response = send_file(image_path) - except Exception: - app.logger.error(f"Could not find image: {image_path}") - return "" - - app.logger.info(f"Showing image >>> '{image_path}'") - # cache indefinitely - response.headers["Cache-Control"] = "max-age=31536000, immutable" - return response - - -@app.route('/toggle-darktheme/', methods=['GET']) -def toggle_darktheme(): - SYSTEM_SETTINGS['darktheme'] = not SYSTEM_SETTINGS['darktheme'] - return redirect(request.args.get("return", "/")) # redirect to the same page URL - - -@app.route('/toggle-sorting/', methods=['GET']) -def toggle_sort(): - SYSTEM_SETTINGS['listsortMTime'] = not SYSTEM_SETTINGS['listsortMTime'] - return redirect("/list") - - -@app.route('/favicon.ico') -def favicon(): - return send_from_directory(os.path.join(app.root_path, 'static'), - 'favicon.ico', mimetype='image/vnd.microsoft.icon') - - -def setup_search(): - search = Search(cfg.search_dir, create=True) - - app.logger.info("Search index creation...") - items = [] - for root, subfolder, files in os.walk(cfg.wiki_directory): - for item in files: - if ( - root.startswith(os.path.join(cfg.wiki_directory, '.git')) or - root.startswith(os.path.join(cfg.wiki_directory, cfg.images_route)) - ): - continue - page_name, ext = os.path.splitext(item) - if ext.lower() != ".md": - continue - path = os.path.relpath(root, cfg.wiki_directory) - items.append((item, page_name, path)) - - search.index_all(cfg.wiki_directory, items) - - -def run_wiki(): - """ - Function that runs the wiki as a Flask app. - """ - if int(cfg.wikmd_logging) == 1: - logging.basicConfig(filename=cfg.wikmd_logging_file, level=logging.INFO) - - if not os.path.exists(UPLOAD_FOLDER_PATH): - app.logger.info(f"Creating upload folder >>> {UPLOAD_FOLDER_PATH}") - os.mkdir(UPLOAD_FOLDER_PATH) - - im.cleanup_images() - setup_search() - app.logger.info("Spawning search indexer watchdog") - watchdog = Watchdog(cfg.wiki_directory, cfg.search_dir) - watchdog.start() - app.run(host=cfg.wikmd_host, port=cfg.wikmd_port, debug=True, use_reloader=False) - -for plugin in plugins: - if "request_html" in dir(plugin): - plugin.request_html(get_html) - -if __name__ == '__main__': - run_wiki()