231 lines
6.6 KiB
Python
231 lines
6.6 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 base64
|
|
import html
|
|
import mimetypes
|
|
import os
|
|
import posixpath
|
|
import re
|
|
from pathlib import Path
|
|
from urllib.parse import unquote
|
|
|
|
from trytond.model import ModelSingleton, ModelSQL, ModelView, fields
|
|
|
|
|
|
DOCUMENTATION_ROOT = Path(__file__).with_name('process_documentation')
|
|
MARKDOWN_LINK = re.compile(r'(!?)\[([^\]]*)\]\(([^)]+)\)')
|
|
|
|
|
|
class ProcessDocumentation(ModelSingleton, ModelSQL, ModelView):
|
|
"Process Documentation"
|
|
__name__ = 'purchase_trade.process.documentation'
|
|
|
|
content = fields.Function(fields.Text('Content'), 'get_content')
|
|
|
|
def get_content(self, name):
|
|
return HTML_TEMPLATE % {
|
|
'content': _documentation_content(),
|
|
}
|
|
|
|
|
|
def _documentation_content():
|
|
sections = []
|
|
index = DOCUMENTATION_ROOT / 'README.md'
|
|
if index.exists():
|
|
sections.append(_render_page(index))
|
|
|
|
for path in sorted(DOCUMENTATION_ROOT.rglob('*.md')):
|
|
if path == index:
|
|
continue
|
|
sections.append(_render_page(path))
|
|
return '\n'.join(sections)
|
|
|
|
|
|
def _render_page(path):
|
|
relative_name = _relative_name(path)
|
|
markdown = path.read_text(encoding='utf-8')
|
|
return '<section id="%s">%s</section>' % (
|
|
_anchor(relative_name), _markdown_to_html(markdown, relative_name))
|
|
|
|
|
|
def _relative_name(path):
|
|
return path.relative_to(DOCUMENTATION_ROOT.resolve()).as_posix()
|
|
|
|
|
|
def _anchor(filename):
|
|
return 'doc-' + re.sub(r'[^a-zA-Z0-9_-]+', '-', filename)
|
|
|
|
|
|
def _resolve_link(current_file, target):
|
|
if target.startswith(('#', 'http://', 'https://', 'mailto:')):
|
|
return target
|
|
target = unquote(target).replace('\\', '/')
|
|
current_dir = posixpath.dirname(current_file)
|
|
return posixpath.normpath(posixpath.join(current_dir, target)).lstrip('/')
|
|
|
|
|
|
def _asset_data_uri(filename):
|
|
path = (DOCUMENTATION_ROOT / Path(*filename.split('/'))).resolve()
|
|
root = DOCUMENTATION_ROOT.resolve()
|
|
common_path = os.path.commonpath([str(root), str(path)])
|
|
if path != root and common_path == str(root):
|
|
if path.exists() and path.is_file():
|
|
mimetype = mimetypes.guess_type(str(path))[0]
|
|
mimetype = mimetype or 'application/octet-stream'
|
|
data = base64.b64encode(path.read_bytes()).decode('ascii')
|
|
return 'data:%s;base64,%s' % (mimetype, data)
|
|
|
|
|
|
def _inline(markdown, current_file):
|
|
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)
|
|
label = html.escape(label)
|
|
|
|
if is_image:
|
|
source = _asset_data_uri(resolved)
|
|
if source:
|
|
parts.append('<img src="%s" alt="%s"/>' % (
|
|
html.escape(source, quote=True), label))
|
|
else:
|
|
parts.append(label)
|
|
elif resolved.endswith('.md'):
|
|
parts.append('<a href="#%s">%s</a>' % (
|
|
html.escape(_anchor(resolved), quote=True), label))
|
|
elif resolved.startswith(('#', 'http://', 'https://', 'mailto:')):
|
|
parts.append('<a href="%s">%s</a>' % (
|
|
html.escape(resolved, quote=True), label))
|
|
else:
|
|
parts.append(html.escape(match.group(0)))
|
|
last = match.end()
|
|
parts.append(html.escape(markdown[last:]))
|
|
return ''.join(parts)
|
|
|
|
|
|
def _markdown_to_html(markdown, current_file):
|
|
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))
|
|
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), 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))
|
|
else:
|
|
close_list()
|
|
body.append('<p>%s</p>' % _inline(stripped, current_file))
|
|
|
|
flush_table()
|
|
close_list()
|
|
|
|
if current_file != 'README.md':
|
|
body.insert(
|
|
0, '<p><a href="#doc-README-md">Back to index</a></p>')
|
|
return '\n'.join(body)
|
|
|
|
|
|
def _render_table(lines, current_file):
|
|
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))
|
|
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))
|
|
output.append('</tr>')
|
|
output.append('</tbody></table>')
|
|
return ''.join(output)
|
|
|
|
|
|
HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<style>
|
|
body {
|
|
color: #1f2933;
|
|
font-family: Arial, sans-serif;
|
|
line-height: 1.5;
|
|
margin: 0;
|
|
padding: 24px;
|
|
}
|
|
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>%(content)s</main>
|
|
</body>
|
|
</html>
|
|
'''
|