Add table of contents to help pages

This commit is contained in:
rubenwardy 2021-02-02 20:05:24 +00:00
parent 5017a9ba7e
commit 14810b2cc5
13 changed files with 143 additions and 52 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,5 @@
title: Help
toc: False
## General Help

View File

@ -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/<username>/<name>/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/<username>/<name>/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/`

View File

@ -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;
}
}

View File

@ -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()

View File

@ -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():

View File

@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=20">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=21">
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
<link rel="shortcut icon" href="/favicon-16.png" sizes="16x16">
<link rel="icon" href="/favicon-128.png" sizes="128x128">

View File

@ -5,9 +5,44 @@
{% endblock %}
{% block container %}
<main class="container mt-4 content">
{% if not page["no_h1"] %}<h1>{{ page['title'] }}</h1>{% endif %}
{{ page.html | safe }}
</main>
{% set html = page.html %}
{% if page.meta.get("toc", True) %}
<div class="container mt-4">
<main class="row">
<article class="col-md-9 content">
{% if not page["no_h1"] %}<h1>{{ page['title'] }}</h1>{% endif %}
{{ html | safe }}
</article>
<nav class="col-md-3 toc">
{% set headings = get_headings(html) %}
<ul class="nav flex-column" role="menu">
{% for item in headings recursive %}
<li class="nav-item">
<a class="nav-link" href="#{{ item.link }}">
{{ item.text }}
</a>
{% if item.children %}
<ul class="nav flex-column" role="menu">
{{ loop(item.children) }}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
</main>
</div>
{% else %}
<div class="container mt-4">
<article class="content">
{% if not page["no_h1"] %}<h1>{{ page['title'] }}</h1>{% endif %}
{{ html | safe }}
</article>
</div>
{% endif %}
{% endblock %}

View File

@ -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