{"title":"JSONHTL → HTML Converter — Source Code","content":[{"heading":{"level":1,"text":"jsonhtl_to_html.py"}},{"para":["Version 3. Fully integrated server-mode support. Canonical source file."]},{"codeblock":{"lang":"python","body":"#!/usr/bin/env python3\nimport os\nimport json\nimport argparse\nimport html\nfrom pathlib import Path\nfrom urllib import request, error\n\n# -----------------------------\n# Utilities\n# -----------------------------\n\ndef escape(text):\n return html.escape(str(text), quote=True)\n\ndef normalise_list(value):\n if isinstance(value, list):\n return value\n return [value]\n\n# -----------------------------\n# Inline Markdown (class-based)\n# -----------------------------\n\ndef render_markdown_inline(text):\n text = str(text)\n result = []\n i = 0\n\n while i < len(text):\n if text.startswith(\"\", i):\n j = text.find(\"\", i + 2)\n if j != -1:\n inner = escape(text[i+2:j])\n result.append(f'<span class=\"md-strong\">{inner}</span>')\n i = j + 2\n continue\n\n if text.startswith(\"\", i):\n j = text.find(\"\", i + 1)\n if j != -1:\n inner = escape(text[i+1:j])\n result.append(f'<span class=\"md-em\">{inner}</span>')\n i = j + 1\n continue\n\n if text.startswith(\"\", i):\n j = text.find(\"\", i + 1)\n if j != -1:\n inner = escape(text[i+1:j])\n result.append(f'<span class=\"md-code\">{inner}</span>')\n i = j + 1\n continue\n\n result.append(escape(text[i]))\n i += 1\n\n return ''.join(result)\n\n\ndef render_inline(elem):\n if isinstance(elem, str):\n return render_markdown_inline(elem)\n\n if not isinstance(elem, dict) or len(elem) != 1:\n return \"\"\n\n key, value = next(iter(elem.items()))\n\n if key == \"link\" and isinstance(value, dict):\n href = str(value.get(\"href\", \"\"))\n text = str(value.get(\"text\", href))\n\n if href.startswith(\"http://\") or href.startswith(\"https://\"):\n target = escape(href)\n else:\n target = escape(href + \\".html\\")\n\n return f'<a href=\"{target}\">{escape(text)}</a>'\n\n if key == \"code\":\n return f'<span class=\"md-code\">{escape(value)}</span>'\n\n return \"\"\n\n# -----------------------------\n# Block Rendering\n# -----------------------------\n\ndef render_para(content):\n parts = []\n for item in normalise_list(content):\n parts.append(render_inline(item))\n return f'<p>{\"\".join(parts)}</p>'\n\n\ndef render_heading(value):\n if not isinstance(value, dict):\n return \"\"\n\n try:\n level = int(value.get(\"level\", 1))\n except Exception:\n level = 1\n\n level = max(1, min(6, level))\n text = escape(value.get(\"text\", \"\"))\n\n return f'<h{level}>{text}</h{level}>'\n\n\ndef render_codeblock(value):\n if not isinstance(value, dict):\n return \"\"\n\n lang = str(value.get(\"lang\", \"\"))\n body = escape(value.get(\"body\", \"\"))\n\n class_attr = f' class=\"language-{escape(lang)}\"' if lang else \"\"\n return f'<pre><code{class_attr}>{body}</code></pre>'\n\n\ndef render_block(block):\n if not isinstance(block, dict) or len(block) != 1:\n return \"\"\n\n key, value = next(iter(block.items()))\n\n if key == \"para\":\n return render_para(value)\n if key == \"heading\":\n return render_heading(value)\n if key == \"codeblock\":\n return render_codeblock(value)\n\n return \"\"\n\n# -----------------------------\n# Document Rendering\n# -----------------------------\n\ndef render_document(doc):\n if not isinstance(doc, dict):\n return \"\"\n\n title = escape(doc.get(\"title\", \"Untitled\"))\n content = doc.get(\"content\", [])\n\n if isinstance(content, str):\n content = [{\"para\": [content]}]\n\n body_parts = []\n for block in content:\n body_parts.append(render_block(block))\n\n body_html = \"\\n\".join(body_parts)\n\n return f\"\"\"<!DOCTYPE html>\n<html lang=\\\"en\\\">\n<head>\n<meta charset=\\\"utf-8\\\">\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1\\\">\n<title>{title}</title>\n<link rel=\\\"stylesheet\\\" href=\\\"/styles.css\\\">\n</head>\n<body>\n<h1>{title}</h1>\n{body_html}\n</body>\n</html>\"\"\"\n\n# -----------------------------\n# Server Fetching\n# -----------------------------\n\ndef fetch_all_notes_from_server(base_url):\n if not isinstance(base_url, str) or not base_url.strip():\n raise ValueError(\"base_url must be a non-empty string\")\n\n try:\n with request.urlopen(base_url) as response:\n status = getattr(response, 'status', None)\n if status is not None and status != 200:\n raise RuntimeError(f\"HTTP request failed with status {status}\")\n raw = response.read()\n except error.URLError as e:\n raise RuntimeError(f\"Failed to connect to server: {e}\") from e\n\n try:\n data = json.loads(raw.decode('utf-8'))\n except Exception as e:\n raise RuntimeError(f\"Failed to parse JSON response: {e}\") from e\n\n if not isinstance(data, dict):\n raise ValueError(\"Server response must be a JSON object mapping keys to documents\")\n\n validated = {}\n for key, doc in data.items():\n if not isinstance(key, str) or not key:\n raise ValueError(f\"Invalid note key: {key!r}\")\n if not isinstance(doc, dict):\n raise ValueError(f\"Document for key '{key}' is not an object\")\n if not isinstance(doc.get('title'), str):\n raise ValueError(f\"Document '{key}' missing valid 'title' string\")\n if not isinstance(doc.get('content'), list):\n raise ValueError(f\"Document '{key}' missing valid 'content' list\")\n validated[key] = doc\n\n return validated\n\n\ndef process_server_notes(base_url, outdir):\n notes = fetch_all_notes_from_server(base_url)\n\n if not isinstance(notes, dict):\n raise TypeError(\"Server response must be a dict mapping keys to documents\")\n\n outdir = Path(outdir)\n outdir.mkdir(parents=True, exist_ok=True)\n\n for key, doc in notes.items():\n if not isinstance(key, str) or not key.strip():\n raise ValueError(f\"Invalid note key: {key!r}\")\n if key.startswith('/'):\n raise ValueError(f\"Note key must not start with '/': {key}\")\n if not isinstance(doc, dict):\n raise TypeError(f\"Document for key {key!r} is not a dict\")\n if 'title' not in doc or 'content' not in doc:\n raise ValueError(f\"Document for key {key!r} missing required fields\")\n\n html_text = render_document(doc)\n write_html(outdir, key, html_text)\n\n# -----------------------------\n# File Processing\n# -----------------------------\n\ndef write_html(outdir, key, html_text):\n path = Path(outdir) / (key + \\".html\\")\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(html_text, encoding=\"utf-8\")\n\n\ndef process_directory(input_dir, outdir):\n for root, _, files in os.walk(input_dir):\n for filename in files:\n if not filename.endswith(\".json\"):\n continue\n\n full_path = Path(root) / filename\n rel_path = full_path.relative_to(input_dir)\n key = str(rel_path.with_suffix(\"\")).replace(os.sep, \\"/\\")\n\n with open(full_path, \"r\", encoding=\"utf-8\") as f:\n doc = json.load(f)\n\n html_text = render_document(doc)\n write_html(outdir, key, html_text)\n\n# -----------------------------\n# Entry Point\n# -----------------------------\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Convert JSONHTL notes to static HTML.\")\n parser.add_argument(\"input\", nargs='?', help=\"Input directory containing JSONHTL .json files\")\n parser.add_argument(\"--outdir\", default=\".\", help=\"Output directory (default: current directory)\")\n parser.add_argument(\"--server\", help=\"Base URL of JSONHTL notes server endpoint\")\n\n args = parser.parse_args()\n\n outdir = Path(args.outdir)\n outdir.mkdir(parents=True, exist_ok=True)\n\n if args.server:\n if args.input:\n raise SystemExit(\"Cannot specify both input directory and --server.\")\n process_server_notes(args.server, outdir)\n return\n\n if not args.input:\n raise SystemExit(\"You must specify either an input directory or --server URL.\")\n\n input_dir = Path(args.input)\n if not input_dir.exists() or not input_dir.is_dir():\n raise SystemExit(\"Input directory does not exist or is not a directory.\")\n\n process_directory(input_dir, outdir)\n\n\nif __name__ == \"__main__\":\n main()\n"}}]}]}