236 lines
7.0 KiB
Python
236 lines
7.0 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
import html
|
|
import mimetypes
|
|
import os
|
|
import posixpath
|
|
import re
|
|
from pathlib import Path
|
|
from urllib.parse import quote, unquote
|
|
|
|
from trytond.protocols.wrappers import (
|
|
HTTPStatus, Response, abort, send_file, with_pool, with_transaction)
|
|
from trytond.wsgi import app
|
|
|
|
|
|
DOCUMENTATION_ROOT = Path(__file__).with_name('process_documentation')
|
|
MARKDOWN_LINK = re.compile(r'(!?)\[([^\]]*)\]\(([^)]+)\)')
|
|
|
|
|
|
@app.route('/<database_name>/purchase_trade/processes', methods={'GET'})
|
|
@app.route(
|
|
'/<database_name>/purchase_trade/processes/<path:filename>',
|
|
methods={'GET'})
|
|
@app.auth_required
|
|
@with_pool
|
|
@with_transaction(user='request', context=dict(_check_access=True))
|
|
def process_documentation(request, pool, filename='README.md'):
|
|
target = _documentation_path(filename)
|
|
if not target.exists() or not target.is_file():
|
|
abort(HTTPStatus.NOT_FOUND)
|
|
|
|
if target.suffix.lower() == '.md':
|
|
relative_name = _relative_name(target)
|
|
markdown = target.read_text(encoding='utf-8')
|
|
content = _markdown_to_html(markdown, relative_name, pool.database_name)
|
|
return Response(content, content_type='text/html; charset=utf-8')
|
|
|
|
mimetype = mimetypes.guess_type(str(target))[0]
|
|
return send_file(
|
|
str(target), request.environ, mimetype=mimetype,
|
|
download_name=target.name)
|
|
|
|
|
|
def _documentation_path(filename):
|
|
filename = unquote(filename or 'README.md').replace('\\', '/')
|
|
filename = posixpath.normpath(filename).lstrip('/')
|
|
if filename in {'.', ''}:
|
|
filename = 'README.md'
|
|
if filename.startswith('../'):
|
|
abort(HTTPStatus.NOT_FOUND)
|
|
|
|
root = DOCUMENTATION_ROOT.resolve()
|
|
target = (root / Path(*filename.split('/'))).resolve()
|
|
common_path = os.path.commonpath([str(root), str(target)])
|
|
if target != root and common_path != str(root):
|
|
abort(HTTPStatus.NOT_FOUND)
|
|
return target
|
|
|
|
|
|
def _relative_name(path):
|
|
return path.relative_to(DOCUMENTATION_ROOT.resolve()).as_posix()
|
|
|
|
|
|
def _documentation_url(database_name, filename):
|
|
filename = posixpath.normpath(filename).lstrip('/')
|
|
return '/%s/purchase_trade/processes/%s' % (
|
|
quote(database_name), quote(filename, safe='/'))
|
|
|
|
|
|
def _resolve_link(current_file, target):
|
|
if target.startswith(('#', 'http://', 'https://', 'mailto:')):
|
|
return target
|
|
current_dir = posixpath.dirname(current_file)
|
|
return posixpath.normpath(posixpath.join(current_dir, target)).lstrip('/')
|
|
|
|
|
|
def _inline(markdown, current_file, database_name):
|
|
parts = []
|
|
last = 0
|
|
for match in MARKDOWN_LINK.finditer(markdown):
|
|
parts.append(html.escape(markdown[last:match.start()]))
|
|
is_image, label, target = match.groups()
|
|
resolved = _resolve_link(current_file, target)
|
|
url = target if resolved == target and target.startswith(
|
|
('#', 'http://', 'https://', 'mailto:')) else _documentation_url(
|
|
database_name, resolved)
|
|
label = html.escape(label)
|
|
url = html.escape(url, quote=True)
|
|
if is_image:
|
|
parts.append('<img src="%s" alt="%s"/>' % (url, label))
|
|
else:
|
|
parts.append('<a href="%s">%s</a>' % (url, label))
|
|
last = match.end()
|
|
parts.append(html.escape(markdown[last:]))
|
|
return ''.join(parts)
|
|
|
|
|
|
def _markdown_to_html(markdown, current_file, database_name):
|
|
body = []
|
|
table = []
|
|
in_list = False
|
|
|
|
def close_list():
|
|
nonlocal in_list
|
|
if in_list:
|
|
body.append('</ul>')
|
|
in_list = False
|
|
|
|
def flush_table():
|
|
nonlocal table
|
|
if table:
|
|
close_list()
|
|
body.append(_render_table(table, current_file, database_name))
|
|
table = []
|
|
|
|
for line in markdown.splitlines():
|
|
stripped = line.strip()
|
|
if stripped.startswith('|') and stripped.endswith('|'):
|
|
table.append(stripped)
|
|
continue
|
|
|
|
flush_table()
|
|
if not stripped:
|
|
close_list()
|
|
continue
|
|
|
|
if stripped.startswith('#'):
|
|
close_list()
|
|
level = len(stripped) - len(stripped.lstrip('#'))
|
|
level = min(level, 6)
|
|
title = stripped[level:].strip()
|
|
body.append('<h%d>%s</h%d>' % (
|
|
level, _inline(title, current_file, database_name), level))
|
|
elif stripped.startswith('- '):
|
|
if not in_list:
|
|
body.append('<ul>')
|
|
in_list = True
|
|
body.append('<li>%s</li>' % _inline(
|
|
stripped[2:].strip(), current_file, database_name))
|
|
else:
|
|
close_list()
|
|
body.append('<p>%s</p>' % _inline(
|
|
stripped, current_file, database_name))
|
|
|
|
flush_table()
|
|
close_list()
|
|
|
|
back_link = ''
|
|
if current_file != 'README.md':
|
|
back_link = '<p><a href="%s">Back to index</a></p>' % html.escape(
|
|
_documentation_url(database_name, 'README.md'), quote=True)
|
|
return HTML_TEMPLATE % {
|
|
'title': html.escape('Tradon Processes'),
|
|
'back_link': back_link,
|
|
'body': '\n'.join(body),
|
|
}
|
|
|
|
|
|
def _render_table(lines, current_file, database_name):
|
|
rows = [
|
|
[cell.strip() for cell in line.strip('|').split('|')]
|
|
for line in lines
|
|
]
|
|
header = rows[0] if rows else []
|
|
data = rows[1:]
|
|
if data and all(set(cell) <= {'-', ':'} for cell in data[0]):
|
|
data = data[1:]
|
|
|
|
output = ['<table>', '<thead><tr>']
|
|
for cell in header:
|
|
output.append('<th>%s</th>' % _inline(
|
|
cell, current_file, database_name))
|
|
output.append('</tr></thead><tbody>')
|
|
for row in data:
|
|
output.append('<tr>')
|
|
for cell in row:
|
|
output.append('<td>%s</td>' % _inline(
|
|
cell, current_file, database_name))
|
|
output.append('</tr>')
|
|
output.append('</tbody></table>')
|
|
return ''.join(output)
|
|
|
|
|
|
HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<title>%(title)s</title>
|
|
<style>
|
|
body {
|
|
color: #1f2933;
|
|
font-family: Arial, sans-serif;
|
|
line-height: 1.5;
|
|
margin: 0;
|
|
padding: 32px;
|
|
}
|
|
main {
|
|
max-width: 1120px;
|
|
}
|
|
a {
|
|
color: #0b63ce;
|
|
text-decoration: none;
|
|
}
|
|
a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
table {
|
|
border-collapse: collapse;
|
|
margin: 16px 0;
|
|
width: 100%%;
|
|
}
|
|
th,
|
|
td {
|
|
border: 1px solid #d7dde5;
|
|
padding: 8px 10px;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
}
|
|
th {
|
|
background: #f3f6f9;
|
|
}
|
|
img {
|
|
height: auto;
|
|
max-width: 100%%;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
%(back_link)s
|
|
%(body)s
|
|
</main>
|
|
</body>
|
|
</html>
|
|
'''
|