Metadata-Version: 2.4
Name: flask-wiki
Version: 3.0.0
Summary: Simple file-based wiki for Flask
Author: RERO+
Author-email: RERO+ <software@rero.ch>
License-Expression: BSD-3-Clause
License-File: LICENSE
Requires-Dist: bootstrap-flask
Requires-Dist: flask
Requires-Dist: flask-wtf
Requires-Dist: babel>=2.16.0
Requires-Dist: flask-babel>=3.0.0
Requires-Dist: wtforms
Requires-Dist: werkzeug>=0.15
Requires-Dist: markdown
Requires-Dist: beautifulsoup4
Requires-Dist: click
Requires-Dist: whoosh-reloaded>=2.7.5
Requires-Dist: markdown-captions>=2.1.2
Requires-Dist: poethepoet
Requires-Python: >=3.14, <3.15
Description-Content-Type: text/markdown

<!--
SPDX-FileCopyrightText: Fondation RERO+
SPDX-License-Identifier: BSD-3-Clause
-->

# Flask-Wiki

A lightweight, file-based wiki system built as a Flask extension. Create, edit, search, and manage wiki pages stored as Markdown files on the filesystem -- no database required.

## Features

- Markdown pages with metadata (title, tags)
- Full-text search powered by Whoosh
- File/image uploads
- WikiLinks (`[[Page Name]]` syntax)
- Multilingual support
- Markdown editor with a server-rendered preview tab
- No CDN: every front-end asset is served by the application
- Customizable templates and permissions

## Installation

```bash
pip install flask-wiki
```

## Quick start

```python
from flask import Flask
from flask_wiki import Wiki

app = Flask(__name__)
app.config["SECRET_KEY"] = "your-secret-key"
Wiki(app)
```

Or using the application factory pattern:

```python
from flask import Flask
from flask_wiki import Wiki

wiki = Wiki()


def create_app():
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "your-secret-key"
    wiki.init_app(app)
    return app
```

The wiki will be available at `/help` by default (configurable via `WIKI_URL_PREFIX`).

### Initialize the search index

Before using search, initialize the Whoosh index:

```bash
flask flask_wiki init-index
flask flask_wiki index
```

## How it works

Wiki pages are plain Markdown files stored in a content directory (`./data` by default). The URL structure mirrors the filesystem, and every page belongs to a language: `/help/guides/setup` maps to `data/guides/setup_en.md` for an English reader. See [Internationalization](#internationalization).

Each page file has an optional metadata header followed by the Markdown body:

```markdown
title: My Page Title
tags: setup, guide

# Content starts here

Regular markdown content...
```

The wiki registers a Flask Blueprint with routes for viewing, editing, searching, and managing pages. Uploaded files (images) are stored in a subfolder and served via middleware.

## Permissions

Flask-Wiki uses a callable-based permission system. The host application provides functions that return `True` or `False` to control access. By default, everything is open (all lambdas return `True`).

There are four permission settings:

| Setting | Purpose |
|---------|---------|
| `WIKI_READ_VIEW_PERMISSION` | Controls access to read routes (view pages, search). Returns 403 if `False`. |
| `WIKI_EDIT_VIEW_PERMISSION` | Controls access to edit routes (edit, delete, upload). Returns 403 if `False`. |
| `WIKI_READ_UI_PERMISSION` | Controls visibility of read-related UI elements in templates. |
| `WIKI_EDIT_UI_PERMISSION` | Controls visibility of edit buttons/links in templates. |

Each permission is a callable (no arguments) that is evaluated per-request. This lets you integrate with any authentication system -- Flask-Login, session-based auth, API tokens, etc.

### Example: integrating with Flask-Login

```python
from flask_login import current_user

app.config["WIKI_READ_VIEW_PERMISSION"] = lambda: current_user.is_authenticated
app.config["WIKI_EDIT_VIEW_PERMISSION"] = lambda: current_user.is_authenticated and current_user.has_role("editor")
app.config["WIKI_EDIT_UI_PERMISSION"] = app.config["WIKI_EDIT_VIEW_PERMISSION"]
```

The `VIEW` permissions are enforced server-side via route decorators. The `UI` permissions only toggle visibility of buttons and links in the templates -- they do not enforce access control on their own. Typically you'll set the UI permissions to match the view permissions, but you can separate them if needed (e.g., show a "log in to edit" button to anonymous users).

## Configuration

### Content & storage

| Key | Default | Description |
|-----|---------|-------------|
| `WIKI_HOME` | `'home'` | Default page for `/` |
| `WIKI_URL_PREFIX` | `'/help'` | URL prefix for the wiki blueprint |
| `WIKI_CONTENT_DIR` | `'./data'` | Directory for Markdown files |
| `WIKI_UPLOAD_FOLDER` | `'./data/files'` | Directory for uploaded images |
| `WIKI_ALLOWED_EXTENSIONS` | `{'png','jpg','jpeg','gif','svg'}` | Allowed upload types |
| `WIKI_INDEX_DIR` | `'./index'` | Whoosh search index directory |

### Templates

All templates can be overridden by setting these config values to your own template paths:

| Key | Default |
|-----|---------|
| `WIKI_BASE_TEMPLATE` | `'wiki/base.html'` |
| `WIKI_PAGE_TEMPLATE` | `'wiki/page.html'` |
| `WIKI_EDITOR_TEMPLATE` | `'wiki/editor.html'` |
| `WIKI_SEARCH_TEMPLATE` | `'wiki/search.html'` |
| `WIKI_FILES_TEMPLATE` | `'wiki/files.html'` |
| `WIKI_NOT_FOUND_TEMPLATE` | `'wiki/404.html'` |
| `WIKI_FORBIDDEN_TEMPLATE` | `'wiki/403.html'` |
| `WIKI_ICON_TEMPLATE` | `'wiki/icons/bootstrap.html'` |

### Front-end assets

The wiki needs no build step, no CDN and no vendored third-party asset. Its
whole front-end comes from `bootstrap-flask`:

| Asset | Origin |
|-------|--------|
| Bootstrap 4, jQuery, Popper | shipped by `bootstrap-flask` |
| Bootstrap Icons (SVG sprite) | shipped by `bootstrap-flask` |

Set `BOOTSTRAP_SERVE_LOCAL = True` so `bootstrap-flask` serves its own assets
instead of a CDN, which is what makes the wiki work without internet access:

```python
app.config["BOOTSTRAP_SERVE_LOCAL"] = True
```

#### Icons

Templates never name a glyph directly. They ask for an *intent* — `search`,
`copy`, `edit`, `upload`, `delete`, `language`, `save` — and `WIKI_ICON_TEMPLATE`
supplies the markup for it:

| Value | Markup | Assets needed |
|-------|--------|---------------|
| `'wiki/icons/bootstrap.html'` (default) | inline SVG using the Bootstrap Icons sprite | none, `bootstrap-flask` ships it |
| `'wiki/icons/fontawesome.html'` | `<i class="fa-solid fa-...">` | Font Awesome 7, supplied by your application |

The Font Awesome variant emits class names only; it bundles nothing. Use it in
an application that already ships Font Awesome — through a webpack bundle, for
instance — and the wiki icons match the rest of that application:

```python
app.config["WIKI_ICON_TEMPLATE"] = "wiki/icons/fontawesome.html"
```

Any template exposing an `icon(name)` macro works, so an application needing
different styles or a third icon set can point the key at its own file.

Pages are edited in a plain `<textarea>`; the *Preview* tab posts the body to
`wiki.preview` and renders it with the same Markdown pipeline as a saved page,
so WikiLinks, captions and syntax highlighting show up exactly as they will.

### Internationalization

| Key | Default | Description |
|-----|---------|-------------|
| `WIKI_CURRENT_LANGUAGE` | `lambda: 'en'` | Callable returning the current language code |
| `WIKI_LANGUAGES` | `{'en': 'English', 'fr': 'French', 'de': 'German', 'it': 'Italian'}` | Available languages |
| `WIKI_FALLBACK_LANGUAGES` | every language of `WIKI_LANGUAGES`, in order | Languages tried, in order, when a page has no variant in the current language |

Every page belongs to a language: its filename carries a language code (`page_fr.md`, `page_de.md`), and `/help/page` serves the variant matching `WIKI_CURRENT_LANGUAGE`. Pages created or edited through the wiki are always saved with a language code.

When a page has no variant in the current language, the wiki walks `WIKI_FALLBACK_LANGUAGES` in order and serves the first translation it finds, with a banner telling the reader which language the page is displayed in. A page is only a 404 when it exists in no language at all. Set `WIKI_FALLBACK_LANGUAGES = []` to disable the cascade.

Page listings (index, tags) and search results follow the same cascade: one entry per page, in the current language when it exists.

Files without a language code (`page.md`) are still served, as a last resort, for wikis created before language codes became mandatory. They are read-only: editing one writes the variant of the current language and leaves the original untouched. To migrate such a wiki, rename its files to `page_<language>.md` and re-run `flask flask_wiki index`.

### Markdown

| Key | Default | Description |
|-----|---------|-------------|
| `WIKI_MARKDOWN_EXTENSIONS` | `{'codehilite', 'fenced_code'}` | Additional Python-Markdown extensions |

The extensions `toc`, `meta`, `tables`, and a built-in Bootstrap extension are always loaded.

## Development

### Requirements

- Python >=3.14,<3.15
- [uv](https://docs.astral.sh/uv/)

### Setup

```bash
git clone <repo-url>
cd flask-wiki
uv sync --frozen
```

### Run the example app

```bash
cd examples
uv run flask flask_wiki init-index
uv run flask flask_wiki index
uv run flask run --debug
# Visit http://localhost:5000/help
```

### Run tests

```bash
uv run poe run_tests
```

## License

BSD 3-Clause. See [LICENSE](LICENSE) for details.
