#!/usr/bin/env python3
"""本地起 HTTP 服务播放教案幻灯片(离线可用)。
用法:python3 docs/teaching/serve.py [端口,默认 8000]
浏览器打开 http://localhost:<端口>/docs/teaching/slides/index.html
只绑定 127.0.0.1(不对局域网暴露)——仓库根目录含 .env(wiki 登录凭据),
必须避免整个仓库被同一 Wi-Fi/局域网上的其他设备访问到。
"""
import html
import http.server
import mimetypes
import os
import socketserver
import sys
import urllib.parse
# 幻灯片里点击的源码链接(.c/.cpp/.py/.md 等)需要以纯文本显示,而不是被当成
# 未知二进制触发下载——默认 mimetypes 对 .cpp/.c 等会猜成 text/x-csrc 之类,
# 浏览器不会内联渲染,这里统一覆盖成 text/plain。
for ext in (".c", ".cpp", ".h", ".hpp", ".cc", ".qrc", ".py", ".md"):
mimetypes.add_type("text/plain", ext)
MARKED_JS_URL = "/docs/teaching/vendor/marked.min.js"
MARKDOWN_TEMPLATE = """
{title}
渲染中...
"""
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.chdir(repo_root)
try:
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
except ValueError:
print(f"用法:python3 {sys.argv[0]} [端口号]")
sys.exit(1)
class ReuseAddrTCPServer(socketserver.TCPServer):
allow_reuse_address = True
class Utf8RequestHandler(http.server.SimpleHTTPRequestHandler):
# http.server 的 guess_type() 从不附带 charset——中文源码/文档被当成
# 无编码信息的 text/plain 返回,浏览器直接导航时会猜错编码变成乱码。
def guess_type(self, path):
ctype = super().guess_type(path)
if ctype.startswith("text/") and "charset=" not in ctype:
ctype += "; charset=utf-8"
return ctype
def end_headers(self):
# 讲义/幻灯片会在课前反复改——不加 no-store 的话,浏览器可能对同一 URL
# 展示改之前缓存的(曾经乱码的)旧响应,看起来像是"修复没生效"。
self.send_header("Cache-Control", "no-store")
super().end_headers()
def do_GET(self):
parsed = urllib.parse.urlsplit(self.path)
# .md 直接导航时渲染成 HTML;?raw=1 保留原始文本入口(复制/diff 用)
if parsed.path.endswith(".md") and "raw=1" not in parsed.query:
self.serve_markdown(parsed.path)
else:
super().do_GET()
def serve_markdown(self, url_path):
fs_path = self.translate_path(url_path)
try:
with open(fs_path, "r", encoding="utf-8") as f:
raw = f.read()
except OSError:
self.send_error(404, "File not found")
return
page = MARKDOWN_TEMPLATE.format(
title=html.escape(os.path.basename(fs_path)),
path_qs=html.escape(url_path),
raw=raw.replace("