docs(infra): 补充中文注释
This commit is contained in:
parent
2db652137d
commit
671a9fea7a
@ -1,10 +1,17 @@
|
||||
//! 前端全局上下文定义。
|
||||
//!
|
||||
//! 当前保存当前登录用户的信息,供 Dioxus 组件在客户端与服务端渲染期间共享访问。
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::models::user::PublicUser;
|
||||
|
||||
/// 用户上下文,用于在组件树中传递登录状态。
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct UserContext {
|
||||
/// 当前登录用户,未登录时为 `None`。
|
||||
pub user: Signal<Option<Arc<PublicUser>>>,
|
||||
/// 是否已完成会话校验,避免重复触发验证请求。
|
||||
pub checked: Signal<bool>,
|
||||
}
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
//! 语法高亮模块。
|
||||
//!
|
||||
//! 仅在 `server` feature 启用时可用,使用 `syntect` 将代码块转换为带 CSS class 的 HTML,
|
||||
//! 配合 `public/highlight.css` 中生成的主题规则实现亮/暗主题高亮。
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub mod server {
|
||||
use std::sync::LazyLock;
|
||||
@ -6,6 +11,7 @@ pub mod server {
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
/// 全局语法集合,懒加载时合并内置语法与 `syntaxes/` 目录下的自定义语法。
|
||||
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(|| {
|
||||
let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
|
||||
if let Err(e) = builder.add_from_folder("syntaxes/", true) {
|
||||
@ -14,16 +20,23 @@ pub mod server {
|
||||
builder.build()
|
||||
});
|
||||
|
||||
/// 根据语言标识查找对应的语法定义。
|
||||
///
|
||||
/// 依次尝试:扩展名、语法名称、小写扩展名/名称、常用别名映射。
|
||||
/// 如果全部失败,则回退到纯文本语法。
|
||||
fn find_syntax(lang: Option<&str>) -> &'static syntect::parsing::SyntaxReference {
|
||||
let ss = &*SYNTAX_SET;
|
||||
if let Some(lang) = lang {
|
||||
if !lang.is_empty() {
|
||||
// 尝试按扩展名匹配
|
||||
if let Some(s) = ss.find_syntax_by_extension(lang) {
|
||||
return s;
|
||||
}
|
||||
// 尝试按语法名称匹配
|
||||
if let Some(s) = ss.find_syntax_by_name(lang) {
|
||||
return s;
|
||||
}
|
||||
// 小写后再匹配一次
|
||||
let lower = lang.to_lowercase();
|
||||
if lower != lang {
|
||||
if let Some(s) = ss.find_syntax_by_extension(&lower) {
|
||||
@ -33,6 +46,7 @@ pub mod server {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
// 常用语言别名映射表
|
||||
let aliases: &[(&str, &str)] = &[
|
||||
("rust", "rs"),
|
||||
("js", "js"),
|
||||
@ -68,6 +82,9 @@ pub mod server {
|
||||
.expect("no plain text syntax")
|
||||
}
|
||||
|
||||
/// 对给定代码字符串按指定语言进行高亮,返回 HTML 字符串。
|
||||
///
|
||||
/// 输出使用 spaced CSS class 风格,便于与 `highlight.css` 中的选择器匹配。
|
||||
pub fn highlight_code(code: &str, lang: Option<&str>) -> String {
|
||||
let trimmed = code.trim();
|
||||
let syntax = find_syntax(lang);
|
||||
@ -75,6 +92,7 @@ pub mod server {
|
||||
let mut generator =
|
||||
ClassedHTMLGenerator::new_with_class_style(syntax, ss, ClassStyle::Spaced);
|
||||
|
||||
// 逐行解析,出错时记录警告并继续
|
||||
for line in LinesWithEndings::from(trimmed) {
|
||||
if let Err(e) = generator.parse_html_for_line_which_includes_newline(line) {
|
||||
tracing::warn!("syntect parse error: {:?}", e);
|
||||
|
||||
@ -1,15 +1,22 @@
|
||||
//! IP 与用户代理信息定期清理后台任务。
|
||||
//!
|
||||
//! 仅在 `server` feature 启用时编译,每天运行一次。
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::db::pool::get_conn;
|
||||
|
||||
/// 启动 IP 信息清理循环,每天将 90 天前的评论的 `ip_address` 与 `user_agent` 置空。
|
||||
pub async fn run_purge() {
|
||||
// 每天触发一次
|
||||
let mut ticker = interval(Duration::from_secs(86400));
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
match get_conn().await {
|
||||
Ok(client) => {
|
||||
// 仅清理 90 天前且仍保留 IP 的评论
|
||||
if let Err(e) = client
|
||||
.execute("UPDATE comments SET ip_address = NULL, user_agent = NULL WHERE created_at < NOW() - INTERVAL '90 days' AND ip_address IS NOT NULL", &[])
|
||||
.await
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
//! 后台任务调度入口。
|
||||
//!
|
||||
//! 所有任务仅在 `server` feature 启用时编译,运行在服务端独立的 tokio 任务中。
|
||||
|
||||
/// 定时清理评论过期的 IP 与用户代理信息,满足隐私保护要求。
|
||||
#[cfg(feature = "server")]
|
||||
pub mod ip_purge;
|
||||
/// 定时删除已过期会话,避免 `sessions` 表无限增长。
|
||||
#[cfg(feature = "server")]
|
||||
pub mod session_cleanup;
|
||||
|
||||
@ -1,15 +1,22 @@
|
||||
//! 会话过期清理后台任务。
|
||||
//!
|
||||
//! 仅在 `server` feature 启用时编译,每小时运行一次。
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::db::pool::get_conn;
|
||||
|
||||
/// 启动会话清理循环,每小时删除 `expires_at < NOW()` 的过期会话。
|
||||
pub async fn run_cleanup() {
|
||||
// 每小时触发一次
|
||||
let mut ticker = interval(Duration::from_secs(3600));
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
match get_conn().await {
|
||||
Ok(client) => {
|
||||
// 删除已过期会话
|
||||
if let Err(e) = client
|
||||
.execute("DELETE FROM sessions WHERE expires_at < NOW()", &[])
|
||||
.await
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
//! 通用工具函数子模块。
|
||||
//!
|
||||
//! `text` 模块仅在 `server` feature 启用时编译;`time` 模块同时提供 WASM 与原生异步版本。
|
||||
|
||||
/// Markdown / 纯文本处理工具。
|
||||
#[cfg(feature = "server")]
|
||||
pub mod text;
|
||||
/// 跨平台的异步睡眠等时间工具。
|
||||
pub mod time;
|
||||
|
||||
@ -1,26 +1,39 @@
|
||||
//! Markdown 与文本处理工具。
|
||||
//!
|
||||
//! 提供移除 Markdown 标记、字数统计、自动生成摘要等功能。
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// 匹配 fenced code block(```...```)的正则。
|
||||
static CODE_BLOCK_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"```[\s\S]*?```").unwrap());
|
||||
|
||||
/// 匹配行内代码(`...`)的正则。
|
||||
static INLINE_CODE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"`[^`]*`").unwrap());
|
||||
|
||||
/// 匹配 Markdown 链接 `[text](url)` 的正则。
|
||||
static LINK_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\[([^\]]*)\]\([^)]*\)").unwrap());
|
||||
|
||||
/// 匹配 Markdown 标题(# 到 ######)的正则。
|
||||
static HEADING_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"^#{1,6}\s*").unwrap());
|
||||
|
||||
/// 匹配 Markdown 图片 `` 的正则。
|
||||
static IMAGE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"!\[([^\]]*)\]\([^)]*\)").unwrap());
|
||||
|
||||
/// 匹配任意空白字符的正则,用于把多个空白合并为单个空格。
|
||||
static WHITESPACE_RE: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new(r"\s+").unwrap());
|
||||
|
||||
/// 去除 Markdown 标记,返回近似纯文本。
|
||||
///
|
||||
/// 处理顺序:代码块 → 行内代码 → 图片 → 链接(保留文字)→ 标题 → 加粗/斜体 → 合并空白。
|
||||
pub fn strip_markdown(md: &str) -> String {
|
||||
let mut plain = CODE_BLOCK_RE.replace_all(md, "").to_string();
|
||||
plain = INLINE_CODE_RE.replace_all(&plain, "").to_string();
|
||||
// Must strip images BEFORE links, otherwise `` becomes `!`
|
||||
// 必须先移除图片再处理链接,否则 `` 会残留 `!`
|
||||
plain = IMAGE_RE.replace_all(&plain, "").to_string();
|
||||
plain = LINK_RE.replace_all(&plain, "$1").to_string();
|
||||
plain = HEADING_RE.replace_all(&plain, "").to_string();
|
||||
@ -33,12 +46,17 @@ pub fn strip_markdown(md: &str) -> String {
|
||||
plain.trim().to_string()
|
||||
}
|
||||
|
||||
/// 统计 Markdown 文本的有效字数。
|
||||
///
|
||||
/// 中文字符每个计 1;英文字母按连续字母串计 1 个词。
|
||||
/// 空文本返回 1,避免摘要或列表中出现 0 字的显示问题。
|
||||
pub fn count_words(md: &str) -> u32 {
|
||||
let plain = strip_markdown(md);
|
||||
let mut count = 0u32;
|
||||
let mut in_word = false;
|
||||
|
||||
for c in plain.chars() {
|
||||
// CJK 统一表意文字范围(基本区)
|
||||
if c as u32 >= 0x4E00 && c as u32 <= 0x9FFF {
|
||||
count += 1;
|
||||
in_word = false;
|
||||
@ -54,6 +72,7 @@ pub fn count_words(md: &str) -> u32 {
|
||||
count.max(1)
|
||||
}
|
||||
|
||||
/// 自动生成文本摘要,取去除 Markdown 后的前 200 个字符。
|
||||
pub fn auto_summary(md: &str) -> String {
|
||||
let plain = strip_markdown(md);
|
||||
plain.chars().take(200).collect()
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
//! 跨平台时间/睡眠工具。
|
||||
//!
|
||||
//! 根据目标架构分别实现:
|
||||
//! - `wasm32`:通过 `js_sys` 调用 JavaScript 的 `setTimeout`。
|
||||
//! - 其他平台:使用 `tokio::time::sleep`。
|
||||
|
||||
/// 异步睡眠指定毫秒数。
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn sleep_ms(ms: u32) {
|
||||
use wasm_bindgen::JsCast;
|
||||
@ -9,6 +16,7 @@ pub async fn sleep_ms(ms: u32) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 异步睡眠指定毫秒数(原生 tokio 版本)。
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn sleep_ms(ms: u32) {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(ms as u64)).await;
|
||||
|
||||
36
src/webp.rs
36
src/webp.rs
@ -1,10 +1,27 @@
|
||||
//! WebP 编解码模块。
|
||||
//!
|
||||
//! 本模块仅在 `server` feature 启用时编译。
|
||||
//!
|
||||
//! ## `zenwebp` 与 `image` crate 的分工
|
||||
//!
|
||||
//! - `image` crate:负责通用图像格式(JPEG、PNG、GIF 等)的解码、缩放、旋转以及
|
||||
//! 像素格式转换(`DynamicImage`、`RgbaImage`、`RgbImage`)。本项目特意禁用了 `image`
|
||||
//! 的 `webp` feature,因为它不支持 WebP 编码,且解码能力有限。
|
||||
//! - `zenwebp`:专门负责 WebP 格式的有损编码与解码。所有需要输出 WebP 或读取 WebP
|
||||
//! 字节流的场景都通过 `zenwebp` 完成。
|
||||
//!
|
||||
//! 简言之:`image` 处理“除 WebP 外的图像操作”,`zenwebp` 处理“WebP 专有编解码”。
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// WebP 编解码过程中可能产生的错误。
|
||||
#[cfg(feature = "server")]
|
||||
#[derive(Debug)]
|
||||
pub enum WebpError {
|
||||
/// 编码失败。
|
||||
Encode(String),
|
||||
/// 解码失败。
|
||||
Decode(String),
|
||||
}
|
||||
|
||||
@ -21,13 +38,20 @@ impl std::fmt::Display for WebpError {
|
||||
#[cfg(feature = "server")]
|
||||
impl std::error::Error for WebpError {}
|
||||
|
||||
/// WebP 有损编码配置。
|
||||
#[cfg(feature = "server")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebpConfig {
|
||||
/// 质量系数,范围 0.0–100.0。
|
||||
pub quality: f32,
|
||||
/// 编码方法,范围 0–6,数值越大压缩率越高但越慢。
|
||||
pub method: u8,
|
||||
}
|
||||
|
||||
/// 从环境变量读取的 WebP 全局配置,未设置时使用默认值。
|
||||
///
|
||||
/// - `WEBP_QUALITY`:默认 85.0,越界时 clamp 到 0.0–100.0。
|
||||
/// - `WEBP_METHOD`:默认 2,越界时 clamp 到 0–6。
|
||||
#[cfg(feature = "server")]
|
||||
pub static WEBP_CONFIG: LazyLock<WebpConfig> = LazyLock::new(|| {
|
||||
let (quality, quality_clamped) = std::env::var("WEBP_QUALITY")
|
||||
@ -68,6 +92,9 @@ pub static WEBP_CONFIG: LazyLock<WebpConfig> = LazyLock::new(|| {
|
||||
WebpConfig { quality, method }
|
||||
});
|
||||
|
||||
/// 将 `image::DynamicImage` 编码为 WebP 字节流。
|
||||
///
|
||||
/// 直接处理 `Rgba8` 与 `Rgb8` 两种像素布局,其他格式先转换为 `Rgba8` 再编码。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn encode(img: &image::DynamicImage, quality: f32, method: u8) -> Result<Vec<u8>, WebpError> {
|
||||
use zenwebp::{EncodeRequest, LossyConfig, PixelLayout};
|
||||
@ -95,13 +122,17 @@ pub fn encode(img: &image::DynamicImage, quality: f32, method: u8) -> Result<Vec
|
||||
do_encode(&config, rgb.as_raw(), PixelLayout::Rgb8, width, height)
|
||||
}
|
||||
_ => {
|
||||
// Convert other formats to RGBA8
|
||||
// 其他像素格式统一转换为 RGBA8 后再交给 zenwebp 编码
|
||||
let rgba = img.to_rgba8();
|
||||
do_encode(&config, rgba.as_raw(), PixelLayout::Rgba8, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 WebP 字节流解码为 `image::DynamicImage`。
|
||||
///
|
||||
/// 根据 alpha 通道信息决定返回 `ImageRgba8` 还是 `ImageRgb8`。
|
||||
/// 解码前会校验像素总数,防止超大图片导致内存问题。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn decode(data: &[u8]) -> Result<image::DynamicImage, WebpError> {
|
||||
use zenwebp::WebPDecoder;
|
||||
@ -116,6 +147,7 @@ pub fn decode(data: &[u8]) -> Result<image::DynamicImage, WebpError> {
|
||||
|
||||
let pixel_count = (width as u64) * (height as u64);
|
||||
|
||||
// 超过最大允许像素数时提前拒绝
|
||||
if pixel_count > crate::api::image::MAX_IMAGE_PIXELS as u64 {
|
||||
return Err(WebpError::Decode(format!(
|
||||
"Image dimensions {}x{} exceed maximum allowed pixels",
|
||||
@ -137,7 +169,7 @@ pub fn decode(data: &[u8]) -> Result<image::DynamicImage, WebpError> {
|
||||
.map(image::DynamicImage::ImageRgba8)
|
||||
.ok_or_else(|| WebpError::Decode("Invalid RGBA dimensions".to_string()))
|
||||
} else {
|
||||
// For RGB output, the buffer is width * height * 3
|
||||
// 无 alpha 时,zenwebp 输出的是 width * height * 3 的 RGB 数据
|
||||
image::RgbImage::from_raw(width, height, output)
|
||||
.map(image::DynamicImage::ImageRgb8)
|
||||
.ok_or_else(|| WebpError::Decode("Invalid RGB dimensions".to_string()))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user