From 8d9085a782cb82a02fd6fff817c2551e4cd9bd11 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Fri, 2 Jan 2026 17:22:23 -0800 Subject: [PATCH 1/7] Add website generator and initial site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tools/gen_website.py: Python script to generate static website - Three tabs: Home, Tutorial (renders TUTORIAL.md), Stdlib browser - Dark theme with syntax highlighting for .cap code - Self-contained HTML output in website/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tools/gen_website.py | 915 +++++++++++++++++++++++++++++++++++++++++++ website/index.html | 817 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 1732 insertions(+) create mode 100644 tools/gen_website.py create mode 100644 website/index.html diff --git a/tools/gen_website.py b/tools/gen_website.py new file mode 100644 index 0000000..557d192 --- /dev/null +++ b/tools/gen_website.py @@ -0,0 +1,915 @@ +#!/usr/bin/env python3 +""" +Generate the Capable programming language website. + +Usage: + python3 tools/gen_website.py + python3 tools/gen_website.py --out website +""" +from __future__ import annotations + +import argparse +import html +import json +import os +import re +from pathlib import Path + + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +STDLIB_ROOT = Path("stdlib/sys") +TUTORIAL_PATH = Path("docs/TUTORIAL.md") + +CAP_KEYWORDS = { + "fn", "let", "if", "else", "while", "for", "return", "match", "struct", + "enum", "impl", "pub", "use", "module", "package", "defer", "capability", + "linear", "copy", "opaque", "extern", "true", "false", "in", "break", + "continue", "drop", "unsafe", "safe", +} + +CAP_TYPES = { + "i32", "u8", "u32", "i64", "u64", "bool", "unit", "string", "Result", + "Ok", "Err", "RootCap", "Console", "Args", "Stdin", "Alloc", "Buffer", + "Slice", "MutSlice", "VecU8", "VecI32", "VecString", "ReadFS", "Filesystem", + "Dir", "FileRead", "Net", "TcpListener", "TcpConn", +} + + +# --------------------------------------------------------------------------- +# Syntax highlighting for .cap code +# --------------------------------------------------------------------------- + +def highlight_cap(code: str) -> str: + """Syntax highlight Capable code.""" + + def escape(s: str) -> str: + return html.escape(s) + + # Tokenize with regex + token_pattern = re.compile( + r'(///.*?$|//.*?$)' # comments + r'|("(?:[^"\\]|\\.)*")' # strings + r'|(\b\d+\b)' # numbers + r'|(\b[a-zA-Z_][a-zA-Z0-9_]*\b)' # identifiers + r'|([^\s\w]+)' # operators/punctuation + r'|(\s+)', # whitespace + re.MULTILINE + ) + + result = [] + for match in token_pattern.finditer(code): + comment, string, number, ident, op, ws = match.groups() + if comment: + result.append(f'{escape(comment)}') + elif string: + result.append(f'{escape(string)}') + elif number: + result.append(f'{escape(number)}') + elif ident: + if ident in CAP_KEYWORDS: + result.append(f'{escape(ident)}') + elif ident in CAP_TYPES: + result.append(f'{escape(ident)}') + else: + result.append(escape(ident)) + elif op: + result.append(escape(op)) + elif ws: + result.append(ws) + + return ''.join(result) + + +# --------------------------------------------------------------------------- +# Markdown to HTML conversion +# --------------------------------------------------------------------------- + +def markdown_to_html(md: str) -> str: + """Convert markdown to HTML with syntax highlighting for cap code blocks.""" + lines = md.split('\n') + html_parts = [] + in_code_block = False + code_lang = "" + code_lines = [] + in_list = False + + def close_list(): + nonlocal in_list + if in_list: + html_parts.append('') + in_list = False + + def process_inline(text: str) -> str: + # Bold + text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) + # Italic + text = re.sub(r'\*(.+?)\*', r'\1', text) + # Inline code + text = re.sub(r'`([^`]+)`', r'\1', text) + # Links + text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1', text) + return text + + i = 0 + while i < len(lines): + line = lines[i] + + # Code block start/end + if line.startswith('```'): + if not in_code_block: + close_list() + in_code_block = True + code_lang = line[3:].strip() + code_lines = [] + else: + code_content = '\n'.join(code_lines) + if code_lang == 'cap': + highlighted = highlight_cap(code_content) + html_parts.append(f'
{highlighted}
') + else: + html_parts.append(f'
{html.escape(code_content)}
') + in_code_block = False + code_lang = "" + i += 1 + continue + + if in_code_block: + code_lines.append(line) + i += 1 + continue + + # Headings + if line.startswith('# '): + close_list() + html_parts.append(f'

{process_inline(line[2:])}

') + elif line.startswith('## '): + close_list() + html_parts.append(f'

{process_inline(line[3:])}

') + elif line.startswith('### '): + close_list() + html_parts.append(f'

{process_inline(line[4:])}

') + # Horizontal rule + elif line.strip() == '---': + close_list() + html_parts.append('
') + # List items + elif line.startswith('- '): + if not in_list: + html_parts.append('