""" App-level routes: about, language, version """ from __future__ import annotations import sys from flask import Blueprint, Response, jsonify, request from pathlib import Path from routes import state from app_config import _set_lang_override, _load_lang_forced bp = Blueprint("app_routes", __name__) _APP_VERSION = (Path(__file__).parent.parent / "VERSION").read_text().strip() _LANG_DIR = (Path(sys._MEIPASS) if getattr(sys, "frozen", False) else Path(__file__).parent.parent) / "lang" @bp.route("/api/about") def about_info(): import platform info = {"python": platform.python_version(), "app": _APP_VERSION} try: import msal as _msal info["msal"] = getattr(_msal, "__version__", "installed") except ImportError: info["msal"] = "not installed" try: import requests as _req info["requests"] = getattr(_req, "__version__", "installed") except ImportError: info["requests"] = "not installed" try: import openpyxl as _xl info["openpyxl"] = getattr(_xl, "__version__", "installed") except ImportError: info["openpyxl"] = "not installed" return jsonify(info) @bp.route("/api/langs") def get_langs(): display_names = { "da": "Dansk", "en": "English", "de": "Deutsch", "fr": "Français", "nl": "Nederlands", "sv": "Svenska", "no": "Norsk", "fi": "Suomi", "es": "Español", "it": "Italiano", "pl": "Polski", "pt": "Português", } langs = [] if _LANG_DIR.exists(): seen = set() for f in sorted(list(_LANG_DIR.glob("*.json")) + list(_LANG_DIR.glob("*.lang"))): code = f.stem if code not in seen: seen.add(code) langs.append({"code": code, "name": display_names.get(code, code.upper())}) langs.sort(key=lambda x: x["code"]) return jsonify({"langs": langs, "current": state.LANG.get("_lang_code", "en")}) @bp.route("/api/set_lang", methods=["POST"]) def set_lang(): data = request.get_json(force=True) or {} code = str(data.get("lang", "en")).strip().lower()[:10] _set_lang_override(code) state.LANG = _load_lang_forced(code) return jsonify({"status": "ok", "lang": code, "translations": state.LANG}) @bp.route("/api/lang") def get_lang_json(): """Return the current language translations as JSON.""" return jsonify(state.LANG) @bp.route("/manual") def manual(): """Serve the user manual as a styled, printable HTML page. Respects ?lang=da|en; falls back to the current UI language.""" import sys as _sys lang = request.args.get("lang", "").strip().lower() or \ state.LANG.get("_lang_code", "da") lang = lang if lang in ("da", "en") else "da" _here = Path(_sys._MEIPASS) if getattr(_sys, "frozen", False) \ else Path(__file__).parent.parent fname = "MANUAL-DA.md" if lang == "da" else "MANUAL-EN.md" md_path = _here / "docs" / "manuals" / fname if not md_path.exists(): return f"Manual file not found: {fname}", 404 md_text = md_path.read_text(encoding="utf-8") body_html = _md_to_html(md_text) title = "GDPR Scanner — Brugermanual" if lang == "da" \ else "GDPR Scanner — User Manual" print_label = "Udskriv" if lang == "da" else "Print" other_lang = "en" if lang == "da" else "da" other_label = "English" if lang == "da" else "Dansk" page = f""" {title}
{title} {other_label}
{body_html} """ return Response(page, mimetype="text/html") def _md_to_html(md: str) -> str: """Lightweight Markdown → HTML converter (no external dependencies). Handles headings, tables, lists, blockquotes, code blocks, bold/italic, inline code, links, and horizontal rules.""" import re, html as _html def inline(text: str) -> str: text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) text = re.sub(r'\*(.+?)\*', r'\1', text) text = re.sub(r'`(.+?)`', lambda m: '' + _html.escape(m.group(1)) + '', text) text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1', text) return text def make_anchor(text: str) -> str: return re.sub(r'[^\w\s-]', '', text.lower()).strip().replace(' ', '-') result = [] lines = md.splitlines() i = 0 in_code = False code_buf = [] in_list = False list_type = None list_buf = [] in_table = False tbl_buf = [] def flush_list(): nonlocal in_list, list_type, list_buf if not in_list: return tag = list_type result.append(f'<{tag}>') for item in list_buf: result.append(f'
  • {inline(item)}
  • ') result.append(f'') in_list = False; list_buf = []; list_type = None def flush_table(): nonlocal in_table, tbl_buf if not in_table or len(tbl_buf) < 2: in_table = False; tbl_buf = []; return heads = [c.strip() for c in tbl_buf[0].strip('|').split('|')] result.append('') result.append('' + ''.join(f'' for h in heads) + '') result.append('') for row in tbl_buf[2:]: cols = [c.strip() for c in row.strip('|').split('|')] result.append('' + ''.join(f'' for c in cols) + '') result.append('
    {inline(h)}
    {inline(c)}
    ') in_table = False; tbl_buf = [] while i < len(lines): line = lines[i] i += 1 # ── fenced code block ────────────────────────────────────────── if line.startswith('```'): if not in_code: flush_list(); flush_table() in_code = True; code_buf = [] else: in_code = False escaped = _html.escape('\n'.join(code_buf)) result.append(f'
    {escaped}
    ') continue if in_code: code_buf.append(line) continue # ── table row ───────────────────────────────────────────────── if line.strip().startswith('|') and '|' in line[1:]: flush_list() in_table = True tbl_buf.append(line) continue elif in_table: flush_table() # ── blank line ──────────────────────────────────────────────── if not line.strip(): flush_list() result.append('') continue # ── heading ─────────────────────────────────────────────────── m = re.match(r'^(#{1,6})\s+(.+)$', line) if m: flush_list() lvl = len(m.group(1)) text = m.group(2) anc = make_anchor(text) result.append(f'{inline(text)}') continue # ── horizontal rule ─────────────────────────────────────────── if re.match(r'^-{3,}$', line.strip()): flush_list() result.append('
    ') continue # ── blockquote ──────────────────────────────────────────────── if line.startswith('> '): flush_list() result.append(f'
    {inline(line[2:])}
    ') continue # ── unordered list ──────────────────────────────────────────── m = re.match(r'^- (.+)$', line) if m: if not in_list or list_type != 'ul': flush_list() in_list = True; list_type = 'ul'; list_buf = [] list_buf.append(m.group(1)) continue # ── ordered list ───────────────────────────────────────────── m = re.match(r'^\d+\. (.+)$', line) if m: if not in_list or list_type != 'ol': flush_list() in_list = True; list_type = 'ol'; list_buf = [] list_buf.append(m.group(1)) continue # ── paragraph ──────────────────────────────────────────────── flush_list() result.append(f'

    {inline(line)}

    ') flush_list() flush_table() return '\n'.join(result)