diff --git a/app/__init__.py b/app/__init__.py index e999201..0027e4e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -21,7 +21,7 @@ import flask_menu as menu from flask_mail import Mail from flask_github import GitHub from flask_wtf.csrf import CSRFProtect -from flask_flatpages import FlatPages, pygments_style_defs +from flask_flatpages import FlatPages from flask_babel import Babel from flask_login import logout_user, current_user, LoginManager import os, redis @@ -29,7 +29,7 @@ import os, redis app = Flask(__name__, static_folder="public/static") app.config["FLATPAGES_ROOT"] = "flatpages" app.config["FLATPAGES_EXTENSION"] = ".md" -app.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = ["fenced_code", "tables", "codehilite"] +app.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = ["fenced_code", "tables", "codehilite", 'toc'] app.config["FLATPAGES_EXTENSION_CONFIG"] = { "fenced_code": {}, "tables": {}, @@ -69,7 +69,7 @@ if not app.debug and app.config["MAIL_UTILS_ERROR_SEND_TO"]: app.logger.addHandler(build_handler(app)) -from .markdown import init_app +from app.utils.markdown import init_app init_app(app) # @babel.localeselector diff --git a/app/blueprints/admin/email.py b/app/blueprints/admin/email.py index 74a360d..eb148b0 100644 --- a/app/blueprints/admin/email.py +++ b/app/blueprints/admin/email.py @@ -21,7 +21,7 @@ from flask_wtf import FlaskForm from wtforms import * from wtforms.validators import * -from app.markdown import render_markdown +from app.utils.markdown import render_markdown from app.models import * from app.tasks.emails import send_user_email from app.utils import rank_required, addAuditLog diff --git a/app/blueprints/api/endpoints.py b/app/blueprints/api/endpoints.py index a5523d3..8d424f1 100644 --- a/app/blueprints/api/endpoints.py +++ b/app/blueprints/api/endpoints.py @@ -19,8 +19,8 @@ from flask_login import current_user, login_required from sqlalchemy.sql.expression import func from app import csrf -from app.markdown import render_markdown -from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Tags, Permission, ForumTopic, MinetestRelease, APIToken, PackageScreenshot +from app.utils.markdown import render_markdown +from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, ForumTopic, MinetestRelease, APIToken, PackageScreenshot from app.querybuilder import QueryBuilder from app.utils import is_package_page from . import bp diff --git a/app/blueprints/users/claim.py b/app/blueprints/users/claim.py index 45e695f..f067435 100644 --- a/app/blueprints/users/claim.py +++ b/app/blueprints/users/claim.py @@ -19,7 +19,7 @@ from flask import redirect, render_template, session, request, flash, url_for from app.models import db, User, UserRank from app.utils import randomString, login_user_set_active from app.tasks.forumtasks import checkForumAccount -from app.tasks.phpbbparser import getProfile +from app.utils.phpbbparser import getProfile import re diff --git a/app/flatpages/help.md b/app/flatpages/help.md index 141c20a..d476297 100644 --- a/app/flatpages/help.md +++ b/app/flatpages/help.md @@ -1,4 +1,5 @@ title: Help +toc: False ## General Help diff --git a/app/flatpages/help/api.md b/app/flatpages/help/api.md index b457d61..88a9e1a 100644 --- a/app/flatpages/help/api.md +++ b/app/flatpages/help/api.md @@ -11,16 +11,13 @@ curl -H "Authorization: Bearer YOURTOKEN" https://content.minetest.net/api/whoam Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/). -## Endpoints - -### Misc - * GET `/api/whoami/` - JSON dictionary with the following keys: * `is_authenticated` - True on successful API authentication * `username` - Username of the user authenticated as, null otherwise. * 4xx status codes will be thrown on unsupported authentication type, invalid access token, or other errors. -### Packages + +## Packages * GET `/api/packages/` - See [Package Queries](#package-queries) * GET `/api/scores/` - See [Package Queries](#package-queries) @@ -42,7 +39,31 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/). * `high_reviewed` - highest reviewed * `tags` -### Releases +### Package Queries + +Example: + + /api/packages/?type=mod&type=game&q=mobs+fun&hide=nonfree&hide=gore + +Supported query parameters: + +* `type` - Package types (`mod`, `game`, `txp`). +* `q` - Query string. +* `author` - Filter by author. +* `tag` - Filter by tags. +* `random` - When present, enable random ordering and ignore `sort`. +* `limit` - Return at most `limit` packages. +* `hide` - Hide content based on [Content Flags](/help/content_flags/). +* `sort` - Sort by (`name`, `title`, `score`, `reviews`, `downloads`, `created_at`, `approved_at`, `last_release`). +* `order` - Sort ascending (`asc`) or descending (`desc`). +* `protocol_version` - Only show packages supported by this Minetest protocol version. +* `engine_version` - Only show packages supported by this Minetest engine version, eg: `5.3.0`. +* `fmt` - How the response is formated. + * `keys` - author/name only. + * `short` - stuff needed for the Minetest client. + + +## Releases * GET `/api/packages///releases/` (List) * Returns array of release dictionaries with keys: @@ -87,7 +108,8 @@ curl -X DELETE https://content.minetest.net/api/packages/username/name/releases/ -H "Authorization: Bearer YOURTOKEN" ``` -### Screenshots + +## Screenshots * GET `/api/packages///screenshots/` (List) * Returns array of screenshot dictionaries with keys: @@ -129,42 +151,14 @@ curl -X POST https://content.minetest.net/api/packages/username/name/screenshots -d "[13, 2, 5, 7]" ``` -### Topics + +## Topics * GET `/api/topics/` - Supports [Package Queries](#package-queries), and the following two options: * `show_added` - Show topics which exist as packages, default true. * `show_discarded` - Show topics which have been marked as outdated, default false. -### Minetest - -* GET `/api/minetest_versions/` - - -## Package Queries - -Example: - - /api/packages/?type=mod&type=game&q=mobs+fun&hide=nonfree&hide=gore - -Supported query parameters: - -* `type` - Package types (`mod`, `game`, `txp`). -* `q` - Query string. -* `author` - Filter by author. -* `tag` - Filter by tags. -* `random` - When present, enable random ordering and ignore `sort`. -* `limit` - Return at most `limit` packages. -* `hide` - Hide content based on [Content Flags](/help/content_flags/). -* `sort` - Sort by (`name`, `title`, `score`, `reviews`, `downloads`, `created_at`, `approved_at`, `last_release`). -* `order` - Sort ascending (`asc`) or descending (`desc`). -* `protocol_version` - Only show packages supported by this Minetest protocol version. -* `engine_version` - Only show packages supported by this Minetest engine version, eg: `5.3.0`. -* `fmt` - How the response is formated. - * `keys` - author/name only. - * `short` - stuff needed for the Minetest client. - - -## Topic Queries +### Topic Queries Example: @@ -178,3 +172,8 @@ Supported query parameters: * `show_added` - Show topics that have an existing package. * `show_discarded` - Show topics marked as discarded. * `limit` - Return at most `limit` topics. + + +## Minetest + +* GET `/api/minetest_versions/` diff --git a/app/scss/components.scss b/app/scss/components.scss index 63a2d93..8e5c6bc 100644 --- a/app/scss/components.scss +++ b/app/scss/components.scss @@ -148,3 +148,24 @@ blockquote { border-bottom: none; } } + +.toc { + .nav-link { + color: #ADADAD; + padding: 0.25rem 0.5rem; + + &:hover, &.active { + color: #DDD; + } + } + + .nav .nav { + margin: 0.1em 0 0.1em 0.7rem; + padding-left: 0.25em; + border-left: 3px solid rgba(173, 173, 173, 0.25); + } + + & > .nav > * > .nav { + margin-bottom: 0.5em; + } +} diff --git a/app/tasks/forumtasks.py b/app/tasks/forumtasks.py index 3844d03..c46ca84 100644 --- a/app/tasks/forumtasks.py +++ b/app/tasks/forumtasks.py @@ -18,7 +18,7 @@ import json, re, sys from app.models import * from app.tasks import celery -from .phpbbparser import getProfile, getTopicsFromForum +from app.utils.phpbbparser import getProfile, getTopicsFromForum import urllib.request @celery.task() diff --git a/app/template_filters.py b/app/template_filters.py index 90597ba..154a5b2 100644 --- a/app/template_filters.py +++ b/app/template_filters.py @@ -6,6 +6,9 @@ from flask_babel import format_timedelta, gettext from urllib.parse import urlparse from datetime import datetime as dt +from .utils.markdown import get_headings + + @app.context_processor def inject_debug(): return dict(debug=app.debug) @@ -13,7 +16,9 @@ def inject_debug(): @app.context_processor def inject_functions(): check_global_perm = Permission.checkPerm - return dict(abs_url_for=abs_url_for, url_set_query=url_set_query, check_global_perm=check_global_perm) + return dict(abs_url_for=abs_url_for, url_set_query=url_set_query, + check_global_perm=check_global_perm, + get_headings=get_headings) @app.context_processor def inject_todo(): diff --git a/app/templates/base.html b/app/templates/base.html index aad4b41..5c4f9a7 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -7,7 +7,7 @@ {% block title %}title{% endblock %} - {{ config.USER_APP_NAME }} - + diff --git a/app/templates/flatpage.html b/app/templates/flatpage.html index 05acb91..d4ba0e0 100644 --- a/app/templates/flatpage.html +++ b/app/templates/flatpage.html @@ -5,9 +5,44 @@ {% endblock %} {% block container %} -
- {% if not page["no_h1"] %}

{{ page['title'] }}

{% endif %} - {{ page.html | safe }} -
+{% set html = page.html %} +{% if page.meta.get("toc", True) %} +
+
+
+ {% if not page["no_h1"] %}

{{ page['title'] }}

{% endif %} + + {{ html | safe }} +
+ + +
+
+{% else %} +
+
+ {% if not page["no_h1"] %}

{{ page['title'] }}

{% endif %} + + {{ html | safe }} +
+
+{% endif %} + {% endblock %} diff --git a/app/markdown.py b/app/utils/markdown.py similarity index 76% rename from app/markdown.py rename to app/utils/markdown.py index bae2bc3..bdf225d 100644 --- a/app/markdown.py +++ b/app/utils/markdown.py @@ -3,6 +3,7 @@ from functools import partial import bleach from bleach import Cleaner from bleach.linkifier import LinkifyFilter +from bs4 import BeautifulSoup from markdown import Markdown from flask import Markup @@ -40,6 +41,10 @@ def allow_class(_tag, name, value): return name == "class" and value in ALLOWED_CSS ALLOWED_ATTRIBUTES = { + "h1": ["id"], + "h2": ["id"], + "h3": ["id"], + "h4": ["id"], "a": ["href", "title"], "img": ["src", "title", "alt"], "code": allow_class, @@ -51,6 +56,7 @@ ALLOWED_PROTOCOLS = ["http", "https", "mailto"] md = None + def render_markdown(source): html = md.convert(source) @@ -61,6 +67,7 @@ def render_markdown(source): filters=[partial(LinkifyFilter, callbacks=bleach.linkifier.DEFAULT_CALLBACKS)]) return cleaner.clean(html) + def init_app(app): global md @@ -69,3 +76,26 @@ def init_app(app): @app.template_filter() def markdown(source): return Markup(render_markdown(source)) + + +def get_headings(html: str): + soup = BeautifulSoup(html, "html.parser") + headings = soup.find_all(["h1", "h2", "h3"]) + + root = [] + stack = [] + for heading in headings: + this = { "link": heading.get("id") or "", "text": heading.text, "children": [] } + this_level = int(heading.name[1:]) - 1 + + while this_level <= len(stack): + stack.pop() + + if len(stack) > 0: + stack[-1]["children"].append(this) + else: + root.append(this) + + stack.append(this) + + return root diff --git a/app/tasks/phpbbparser.py b/app/utils/phpbbparser.py similarity index 100% rename from app/tasks/phpbbparser.py rename to app/utils/phpbbparser.py