Files
AzureAD\SylvainDUVERNAY 092fe138c8 Add Tradon processes help menu
2026-05-29 12:41:57 +02:00

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