# 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 from trytond.transaction import Transaction DOCUMENTATION_ROOT = Path(__file__).with_name('process_documentation') MARKDOWN_LINK = re.compile(r'(!?)\[([^\]]*)\]\(([^)]+)\)') HTML_IMAGE_SOURCE = re.compile( r'(]*\bsrc=)(["\'])([^"\']+)(\2)', re.IGNORECASE) class ProcessDocumentation(ModelSingleton, ModelSQL, ModelView): "Process Documentation" __name__ = 'purchase_trade.process.documentation' content = fields.Function(fields.Text('Content'), 'get_content') @staticmethod def default_content(): return _html_content() def get_content(self, name): return _html_content() def _html_content(): html_file = Transaction().context.get('process_documentation_html') if html_file: return _html_file_content(html_file) document = HTML_TEMPLATE % { 'content': _documentation_content(), } return IFRAME_TEMPLATE % { 'document': html.escape(document, quote=True), } def _html_file_content(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() and path.suffix.lower() == '.html': document = path.read_text(encoding='utf-8') document = _inline_html_assets(document, _relative_name(path)) return IFRAME_TEMPLATE % { 'document': html.escape(document, quote=True), } return _html_error('Documentation page not found: %s' % filename) def _inline_html_assets(document, current_file): def replace(match): prefix, quote, source, suffix = match.groups() resolved = _resolve_link(current_file, source) if resolved.startswith(('http://', 'https://', 'data:', 'mailto:')): return match.group(0) data_uri = _asset_data_uri(resolved) if not data_uri: return match.group(0) return '%s%s%s%s' % (prefix, quote, html.escape(data_uri, quote=True), suffix) return HTML_IMAGE_SOURCE.sub(replace, document) def _html_error(message): document = HTML_TEMPLATE % { 'content': '

%s

' % html.escape(message), } return IFRAME_TEMPLATE % { 'document': html.escape(document, quote=True), } 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 '
%s
' % ( _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('%s' % ( html.escape(source, quote=True), label)) else: parts.append(label) elif resolved.endswith('.md'): parts.append(_internal_link(_anchor(resolved), label)) elif resolved.startswith('#'): parts.append(_internal_link(resolved.lstrip('#'), label)) elif resolved.startswith(('http://', 'https://', 'mailto:')): parts.append('%s' % ( 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('') 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('%s' % ( level, _inline(title, current_file), level)) elif stripped.startswith('- '): if not in_list: body.append('