From cf8eba082fb5a9876f08be1a38eb809dd2571615 Mon Sep 17 00:00:00 2001 From: cnb Date: Sun, 5 Jul 2026 11:48:18 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=A8=20marknb=20=E6=9B=BF=E6=8D=A2=20Pytho?= =?UTF-8?q?n=20markdown=EF=BC=8C=E5=B9=B6=E6=B7=BB=E5=8A=A0=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E5=AE=89=E8=A3=85=E4=B8=8E=E5=8D=87=E7=BA=A7=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 部署时通过 install.sh 自动编译 marknb;日常更新用 upgrade.sh 拉代码并重编译。 Co-authored-by: Cursor --- .cnb.yml | 10 ++----- .gitignore | 3 ++ README.md | 61 +++++++++++++++++++++++++++++++++++--- blog.py | 61 ++++++++++++++++++++++++++++++-------- requirements.txt | 1 - scripts/common.sh | 74 ++++++++++++++++++++++++++++++++++++++++++++++ scripts/install.sh | 21 +++++++++++++ scripts/upgrade.sh | 21 +++++++++++++ 8 files changed, 227 insertions(+), 25 deletions(-) create mode 100755 scripts/common.sh create mode 100755 scripts/install.sh create mode 100755 scripts/upgrade.sh diff --git a/.cnb.yml b/.cnb.yml index 07ffaba..45ae02a 100644 --- a/.cnb.yml +++ b/.cnb.yml @@ -3,11 +3,5 @@ $: - runner: cpus: 2 stages: - - name: ls - script: ls -al - - name: install dependents - script: - - apt install python3 python3-pip python3-venv -y - - python3 -m venv ./venv - - ./venv/bin/pip install -r requirements.txt - - echo "source $(pwd)/venv/bin/activate" >> ~/.zshrc \ No newline at end of file + - name: install + script: bash scripts/install.sh diff --git a/.gitignore b/.gitignore index da68087..7991baf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ __pycache__/ # C extensions *.so +# marknb (built by scripts/install.sh) +vendor/ + # Distribution / packaging .Python build/ diff --git a/README.md b/README.md index 56d1fe9..6a2d94e 100644 --- a/README.md +++ b/README.md @@ -43,16 +43,40 @@ - 单管理员后台:密码登录、CSRF、Session,够用且不繁琐。 - 备案号挂载:`BLOG_ICP_TEXT` / `BLOG_ICP_URL` 环境变量直接生效,国内合规。 - 反代友好:默认监听 `127.0.0.1:8765`,套 Nginx 即可上 HTTPS。 +- Markdown 引擎:[marknb](https://cnb.cool/whcself/marknb) — 零堆分配 C 解析器,比 Python `markdown` 更快,支持 GFM 表格与 LaTeX 透传。 ### 30 秒上手 ```bash -git clone https://github.com/WHC2006/py-blog.git +git clone https://cnb.cool/whcself/py-blog.git cd py-blog -pip install -r requirements.txt +bash scripts/install.sh +source venv/bin/activate python blog.py ``` +`install.sh` 会自动从 [marknb](https://cnb.cool/whcself/marknb) 拉取源码、编译为 `vendor/marknb/libmarknb.so`,并创建 `venv` 安装 Python 依赖。 + +### 升级 + +已部署的站点拉取新版本后,一条命令完成代码、marknb 与 pip 依赖更新: + +```bash +cd py-blog +bash scripts/upgrade.sh +source venv/bin/activate +python blog.py # 重启服务 +``` + +`upgrade.sh` 会依次:`git pull` py-blog → 重编译 marknb → `pip install -U` 依赖。若尚未安装过,会自动回退执行 `install.sh`。 + +### 脚本一览 + +| 脚本 | 用途 | +| --- | --- | +| `scripts/install.sh` | 首次部署:系统依赖 + marknb 编译 + venv | +| `scripts/upgrade.sh` | 日常升级:拉代码 + 重编译 marknb + 更新 pip | + 打开 ,登录入口在 `/admin/login`。 若没设置 `BLOG_ADMIN_PASSWORD`,**首次启动会在终端打印一次随机密码**,请立刻收好。 @@ -68,6 +92,7 @@ python blog.py | `BLOG_SECRET_KEY` | — | Session 签名密钥;未设置则自动生成并持久化 | | `BLOG_ICP_TEXT` | — | 页脚备案文案,例如 `京ICP备xxxxxx号-1` | | `BLOG_ICP_URL` | — | 备案查询链接,例如 `https://beian.miit.gov.cn/` | +| `MARKNB_LIB` | `vendor/marknb/libmarknb.so` | marknb 动态库路径(可选) | ### Nginx 反代示例 @@ -143,16 +168,43 @@ But beginners also deserve a blog: - Built-in ICP footer (`BLOG_ICP_TEXT` / `BLOG_ICP_URL`) for hosting in China. - Reverse-proxy friendly: binds to `127.0.0.1:8765` by default; put Nginx in front for HTTPS. +- Markdown engine: [marknb](https://cnb.cool/whcself/marknb) — zero-malloc C + parser, faster than Python `markdown`, with GFM tables and LaTeX passthrough. ### Quick start ```bash -git clone https://github.com/WHC2006/py-blog.git +git clone https://cnb.cool/whcself/py-blog.git cd py-blog -pip install -r requirements.txt +bash scripts/install.sh +source venv/bin/activate python blog.py ``` +`install.sh` clones [marknb](https://cnb.cool/whcself/marknb), builds +`vendor/marknb/libmarknb.so`, and creates a `venv` with Python deps. + +### Upgrade + +On an existing deployment, one command updates code, marknb, and pip packages: + +```bash +cd py-blog +bash scripts/upgrade.sh +source venv/bin/activate +python blog.py # restart the service +``` + +`upgrade.sh` runs `git pull` → rebuild marknb → `pip install -U`. Falls back +to `install.sh` if no `venv` exists yet. + +### Scripts + +| Script | Purpose | +| --- | --- | +| `scripts/install.sh` | First deploy: system deps + marknb build + venv | +| `scripts/upgrade.sh` | Routine upgrade: pull + rebuild marknb + pip update | + Open . Admin login lives at `/admin/login`. If `BLOG_ADMIN_PASSWORD` is unset, **a random password is printed once** to the terminal on first boot — copy it immediately. @@ -169,6 +221,7 @@ the terminal on first boot — copy it immediately. | `BLOG_SECRET_KEY` | — | Session signing key; auto-generated and persisted if unset | | `BLOG_ICP_TEXT` | — | Footer ICP text (China hosting) | | `BLOG_ICP_URL` | — | Footer ICP link | +| `MARKNB_LIB` | `vendor/marknb/libmarknb.so` | Path to marknb shared library (optional) | ### Nginx reverse proxy diff --git a/blog.py b/blog.py index f4ec945..3e8e284 100644 --- a/blog.py +++ b/blog.py @@ -2,8 +2,8 @@ 单文件博客:FastAPI + SQLite + Jinja2,Markdown 预渲染为 HTML。 安装依赖: - pip install -r requirements.txt - # 或: pip install fastapi "uvicorn[standard]" jinja2 python-multipart markdown + bash scripts/install.sh + # 或手动: 编译 vendor/marknb/libmarknb.so 后 pip install -r requirements.txt 运行: python blog.py @@ -26,18 +26,18 @@ from __future__ import annotations import asyncio +import ctypes import hashlib import os import re import secrets import sqlite3 +import sys from contextlib import contextmanager from datetime import datetime, timezone from pathlib import Path from typing import Any, Optional from urllib.parse import quote - -import markdown from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile from fastapi.responses import HTMLResponse, RedirectResponse from jinja2 import BaseLoader, Environment, TemplateNotFound @@ -193,19 +193,56 @@ def bootstrap_secrets() -> str: # ----------------------------------------------------------------------------- -# Markdown → HTML +# Markdown → HTML (marknb: https://cnb.cool/whcself/marknb) # ----------------------------------------------------------------------------- -_MD_EXTENSIONS = [ - "markdown.extensions.fenced_code", - "markdown.extensions.tables", - "markdown.extensions.nl2br", - "markdown.extensions.sane_lists", -] +_MARKNB_DEFAULT = 65536 + + +def _load_marknb() -> ctypes.CDLL: + lib_path = os.environ.get("MARKNB_LIB") + if not lib_path: + here = Path(__file__).resolve().parent + for candidate in ( + here / "vendor" / "marknb" / "libmarknb.so", + Path("/usr/local/lib/libmarknb.so"), + ): + if candidate.is_file(): + lib_path = str(candidate) + break + if not lib_path: + print( + "找不到 marknb 动态库。请先运行: bash scripts/install.sh(升级: bash scripts/upgrade.sh)", + file=sys.stderr, + ) + sys.exit(1) + + lib = ctypes.CDLL(lib_path) + lib.marknb_parse.argtypes = [ + ctypes.c_char_p, + ctypes.c_size_t, + ctypes.c_char_p, + ctypes.c_size_t, + ctypes.POINTER(ctypes.c_size_t), + ] + lib.marknb_parse.restype = ctypes.c_int + return lib + + +_MARKNB = _load_marknb() def render_markdown(md_text: str) -> str: - return markdown.markdown(md_text, extensions=_MD_EXTENSIONS) + md = md_text.encode("utf-8") + cap = max(len(md) * 4 + 256, _MARKNB_DEFAULT) + limit = len(md) * 64 + 4096 + while cap <= limit: + buf = ctypes.create_string_buffer(cap) + out_len = ctypes.c_size_t(0) + if _MARKNB.marknb_parse(md, len(md), buf, cap, ctypes.byref(out_len)) == 0: + return buf.raw[: out_len.value].decode("utf-8") + cap *= 2 + raise RuntimeError("marknb: output buffer exhausted") # ----------------------------------------------------------------------------- diff --git a/requirements.txt b/requirements.txt index e5c742b..ec8435d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,4 @@ fastapi>=0.110,<1 uvicorn[standard]>=0.27,<1 jinja2>=3.1,<4 python-multipart>=0.0.9,<1 -markdown>=3.5,<4 itsdangerous diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100755 index 0000000..68b572c --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# marknb / py-blog 公共构建逻辑(供 install.sh、upgrade.sh 引用) +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VENDOR="$ROOT/vendor/marknb" +MARKNB_REPO="${MARKNB_REPO:-https://cnb.cool/whcself/marknb.git}" +MARKNB_SRC="$VENDOR/src" +LIB_PATH="$VENDOR/libmarknb.so" + +apt_install() { + if ! command -v apt-get >/dev/null 2>&1; then + return 0 + fi + local pkgs=(gcc make git python3 python3-pip python3-venv) + if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" -ne 0 ]; then + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends "${pkgs[@]}" + else + apt-get update -qq + apt-get install -y --no-install-recommends "${pkgs[@]}" + fi +} + +build_marknb() { + mkdir -p "$VENDOR" + if [ -d "$MARKNB_SRC/.git" ]; then + git -C "$MARKNB_SRC" fetch --depth 1 origin main + git -C "$MARKNB_SRC" checkout -f main + git -C "$MARKNB_SRC" reset --hard origin/main + else + git clone --depth 1 --branch main "$MARKNB_REPO" "$MARKNB_SRC" + fi + + local srcs=( + "$MARKNB_SRC/src/fsm.c" + "$MARKNB_SRC/src/table_stats.c" + "$MARKNB_SRC/src/line_index.c" + "$MARKNB_SRC/src/render.c" + "$MARKNB_SRC/src/marknb.c" + "$MARKNB_SRC/src/compat_cmark.c" + ) + gcc -std=c11 -Wall -Wextra -O3 -fPIC -shared \ + -I"$MARKNB_SRC/include" -I"$MARKNB_SRC/src" \ + "${srcs[@]}" -lm -o "$LIB_PATH" +} + +install_python() { + if [ ! -d "$ROOT/venv" ]; then + python3 -m venv "$ROOT/venv" + fi + "$ROOT/venv/bin/pip" install -U pip + "$ROOT/venv/bin/pip" install -r "$ROOT/requirements.txt" +} + +upgrade_python() { + if [ ! -d "$ROOT/venv" ]; then + echo "未找到 venv,改为执行 install.sh" >&2 + exec bash "$ROOT/scripts/install.sh" + fi + "$ROOT/venv/bin/pip" install -U pip + "$ROOT/venv/bin/pip" install -U -r "$ROOT/requirements.txt" +} + +pull_py_blog() { + if [ ! -d "$ROOT/.git" ]; then + echo "当前目录不是 git 仓库,跳过 py-blog 代码拉取" + return 0 + fi + local branch + branch="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)" + git -C "$ROOT" fetch origin "$branch" + git -C "$ROOT" merge --ff-only "origin/$branch" +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..73bf398 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# 一键安装 py-blog 依赖:编译 marknb 动态库 + Python venv +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=common.sh +source "$ROOT/scripts/common.sh" + +echo "==> 安装系统构建依赖(如有 apt)" +apt_install + +echo "==> 拉取并编译 marknb → $LIB_PATH" +build_marknb + +echo "==> 安装 Python 依赖到 venv" +install_python + +echo "" +echo "安装完成。" +echo " marknb: $LIB_PATH" +echo " 启动: source venv/bin/activate && python blog.py" diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh new file mode 100755 index 0000000..1135aa1 --- /dev/null +++ b/scripts/upgrade.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# 升级已部署的 py-blog:拉取代码、重编译 marknb、更新 Python 依赖 +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=common.sh +source "$ROOT/scripts/common.sh" + +echo "==> 拉取 py-blog 最新代码" +pull_py_blog + +echo "==> 重新编译 marknb → $LIB_PATH" +build_marknb + +echo "==> 升级 Python 依赖" +upgrade_python + +echo "" +echo "升级完成。" +echo " marknb: $LIB_PATH" +echo " 若 blog.py 正在运行,请重启服务使变更生效。"