Compare commits
4 Commits
6276bab6c6
...
b25abcb9d8
| Author | SHA1 | Date | |
|---|---|---|---|
| b25abcb9d8 | |||
| 644db48d17 | |||
| db74a96de1 | |||
| 17d596c512 |
32
Cargo.lock
generated
32
Cargo.lock
generated
@ -1742,7 +1742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2964,9 +2964,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
@ -3612,7 +3612,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3776,9 +3776,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.4"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -3788,9 +3788,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -3901,7 +3901,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4252,9 +4252,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.4"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
@ -4378,9 +4378,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -4576,9 +4576,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
version = "1.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@ -5260,7 +5260,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -13,13 +13,13 @@ required-features = ["server"]
|
||||
[dependencies]
|
||||
dioxus = { version = "0.7.9", features = ["fullstack", "router"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.52", features = ["rt-multi-thread", "macros", "fs", "time", "sync"], optional = true }
|
||||
tokio = { version = "1.53", features = ["rt-multi-thread", "macros", "fs", "time", "sync"], optional = true }
|
||||
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"], optional = true }
|
||||
deadpool-postgres = { version = "0.14", optional = true }
|
||||
argon2 = { version = "0.5", optional = true }
|
||||
uuid = { version = "1", features = ["v4"], optional = true }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
regex = { version = "1.12", optional = true }
|
||||
regex = { version = "1.13", optional = true }
|
||||
# mhchem 化学公式转译器(src/api/mhchem.rs)需要 lookahead 正则,
|
||||
# Rust `regex` crate 不支持 `(?=...)`/`(?!...)`,故用 `fancy-regex`。
|
||||
# 该 crate 已被 syntect 间接引入(0.16.2,见 Cargo.lock),此处显式声明为直接依赖。
|
||||
@ -77,7 +77,7 @@ js-sys = "0.3"
|
||||
serde-wasm-bindgen = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.52", features = ["rt-multi-thread", "macros", "fs", "time", "sync"] }
|
||||
tokio = { version = "1.53", features = ["rt-multi-thread", "macros", "fs", "time", "sync"] }
|
||||
serial_test = "3"
|
||||
|
||||
[profile.release]
|
||||
|
||||
21
build.rs
21
build.rs
@ -19,9 +19,24 @@ fn main() {
|
||||
// 工作区脏状态变化时 describe 的 --dirty 后缀也会变,重跑一次。
|
||||
println!("cargo:rerun-if-changed=.git/index");
|
||||
|
||||
set_env("YGG_BUILD_GIT_DESCRIBE", env_or_git("YGG_BUILD_GIT_DESCRIBE", &["describe", "--tags", "--always", "--dirty"]));
|
||||
set_env("YGG_BUILD_GIT_HASH", env_or_git("YGG_BUILD_GIT_HASH", &["rev-parse", "HEAD"]));
|
||||
set_env("YGG_BUILD_GIT_COMMIT_DATE", env_or_git("YGG_BUILD_GIT_COMMIT_DATE", &["log", "-1", "--format=%cd", "--date=iso-strict"]));
|
||||
set_env(
|
||||
"YGG_BUILD_GIT_DESCRIBE",
|
||||
env_or_git(
|
||||
"YGG_BUILD_GIT_DESCRIBE",
|
||||
&["describe", "--tags", "--always", "--dirty"],
|
||||
),
|
||||
);
|
||||
set_env(
|
||||
"YGG_BUILD_GIT_HASH",
|
||||
env_or_git("YGG_BUILD_GIT_HASH", &["rev-parse", "HEAD"]),
|
||||
);
|
||||
set_env(
|
||||
"YGG_BUILD_GIT_COMMIT_DATE",
|
||||
env_or_git(
|
||||
"YGG_BUILD_GIT_COMMIT_DATE",
|
||||
&["log", "-1", "--format=%cd", "--date=iso-strict"],
|
||||
),
|
||||
);
|
||||
set_env("YGG_BUILD_RUSTC_VERSION", rustc_version());
|
||||
// 编译时刻(Unix 秒)。std 无 ISO 8601 格式化,存秒数由运行时 chrono 解析。
|
||||
set_env("YGG_BUILD_TIME", build_time_unix());
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
// 直接赋给 window.MermaidRenderer 作为构建时副作用,使 window.MermaidRenderer
|
||||
// 本身即 mermaid API(initialize/render)。
|
||||
import mermaid from 'mermaid';
|
||||
|
||||
// 构建时副作用:把 mermaid 挂到全局。ES module 的顶层赋值会被 vite 保留。
|
||||
(globalThis as unknown as { MermaidRenderer: typeof mermaid }).MermaidRenderer = mermaid;
|
||||
|
||||
|
||||
@ -16,15 +16,12 @@ import { beforeEach, describe, expect, it } from 'vitest';
|
||||
*/
|
||||
|
||||
const UNESCAPE_RE = /\\\[\^([^\n\\]*?)\\\]/g;
|
||||
const unescape = (md: string) => md.replace(UNESCAPE_RE, '[^$1]');
|
||||
const unescapeFootnote = (md: string) => md.replace(UNESCAPE_RE, '[^$1]');
|
||||
|
||||
function makeEditor() {
|
||||
return new Editor({
|
||||
element: document.body,
|
||||
extensions: [
|
||||
StarterKit.configure({ heading: { levels: [1, 2, 3] } }),
|
||||
Markdown,
|
||||
],
|
||||
extensions: [StarterKit.configure({ heading: { levels: [1, 2, 3] } }), Markdown],
|
||||
content: '',
|
||||
});
|
||||
}
|
||||
@ -44,31 +41,31 @@ describe('脚注语法 - escapeMarkdownSyntax 破坏与修复', () => {
|
||||
});
|
||||
|
||||
it('unescape 正则还原脚注引用 \\[^1\\] → [^1]', () => {
|
||||
expect(unescape('\\[^1\\]')).toBe('[^1]');
|
||||
expect(unescapeFootnote('\\[^1\\]')).toBe('[^1]');
|
||||
});
|
||||
|
||||
it('unescape 正则还原含空格的脚注 label', () => {
|
||||
expect(unescape('\\[^my note\\]')).toBe('[^my note]');
|
||||
expect(unescapeFootnote('\\[^my note\\]')).toBe('[^my note]');
|
||||
});
|
||||
|
||||
it('unescape 正则还原脚注定义 \\[^1\\]: → [^1]:', () => {
|
||||
expect(unescape('\\[^1\\]: 定义内容')).toBe('[^1]: 定义内容');
|
||||
expect(unescapeFootnote('\\[^1\\]: 定义内容')).toBe('[^1]: 定义内容');
|
||||
});
|
||||
|
||||
it('unescape 正则不影响普通链接 [text](url)', () => {
|
||||
const md = '\\[text\\](https://example.com)';
|
||||
// 普通链接的 \[text\] 不以 ^ 开头,不会被误改
|
||||
expect(unescape(md)).toBe(md);
|
||||
expect(unescapeFootnote(md)).toBe(md);
|
||||
});
|
||||
|
||||
it('unescape 正则一次处理多个脚注引用', () => {
|
||||
const md = '引用了\\[^1\\]和\\[^pc\\]和\\[^2\\]';
|
||||
expect(unescape(md)).toBe('引用了[^1]和[^pc]和[^2]');
|
||||
expect(unescapeFootnote(md)).toBe('引用了[^1]和[^pc]和[^2]');
|
||||
});
|
||||
|
||||
it('完整往返:unescape(getMarkdown()) 保留脚注引用语法', () => {
|
||||
editor.commands.setContent('正文[^1]', { contentType: 'markdown' });
|
||||
const fixed = unescape(editor.getMarkdown());
|
||||
const fixed = unescapeFootnote(editor.getMarkdown());
|
||||
expect(fixed).toContain('[^1]');
|
||||
expect(fixed).not.toContain('\\[^1\\]');
|
||||
});
|
||||
|
||||
@ -234,7 +234,9 @@ class TiptapEditorInstance {
|
||||
const proseMirrorDom = this.editor.view.dom;
|
||||
if (!this.isSourceMode) {
|
||||
// 富文本 → 源码:导出当前 Markdown 到 textarea
|
||||
this.sourceTextarea.value = TiptapEditorInstance.unescapeFootnoteSyntax(this.editor.getMarkdown());
|
||||
this.sourceTextarea.value = TiptapEditorInstance.unescapeFootnoteSyntax(
|
||||
this.editor.getMarkdown(),
|
||||
);
|
||||
// 必须在 display:'none' 之前读取滚动比例——隐藏后 scrollTop 会被浏览器归零
|
||||
const pmRatio = this.getScrollRatio(proseMirrorDom);
|
||||
proseMirrorDom.style.display = 'none';
|
||||
|
||||
@ -417,26 +417,6 @@ pub async fn get_user_by_token(token: &str) -> Result<Option<SessionUser>, Serve
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
/// 使指定用户的所有 session 立即失效:bump `session_generation`。
|
||||
///
|
||||
/// 用于角色降级、封禁、密码修改等场景。bump 后该用户所有已签发 session 在下次
|
||||
/// `get_user_by_token` 时因世代不匹配被逐出缓存并视为未登录。内存缓存无需主动清,
|
||||
/// 惰性逐出即可。当前仓库无运行时角色变更入口,本函数是为未来「用户管理」功能
|
||||
/// 预备的基础设施,一旦引入降级/封禁的 server function,必须在 UPDATE 后调用。
|
||||
#[allow(dead_code)] // 预留给未来的用户管理功能(角色变更/封禁触发全量 session 失效)
|
||||
pub async fn invalidate_user_sessions(user_id: i32) -> Result<(), ServerFnError> {
|
||||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE users SET session_generation = session_generation + 1 WHERE id = $1",
|
||||
&[&user_id],
|
||||
)
|
||||
.await
|
||||
.map_err(AppError::query)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取当前登录用户的公开信息。
|
||||
///
|
||||
/// Dioxus server function,注册在 `/api` 路径下。
|
||||
|
||||
@ -430,9 +430,11 @@ mod tests {
|
||||
"源码恰好等于上限应放行"
|
||||
);
|
||||
let over = "a".repeat(max + 1);
|
||||
let err =
|
||||
validate_exec_request(&req("python", &over)).expect_err("超出上限应拒绝");
|
||||
assert!(err.to_string().contains("过大"), "错误信息应提及大小: {err}");
|
||||
let err = validate_exec_request(&req("python", &over)).expect_err("超出上限应拒绝");
|
||||
assert!(
|
||||
err.to_string().contains("过大"),
|
||||
"错误信息应提及大小: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -350,11 +350,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_query_source_rejects_unparseable_sql() {
|
||||
for bad in ["query:这不是SQL", "query:SELECT FROM", "query:@#$%", "query:1+1"] {
|
||||
assert!(
|
||||
parse_source(bad).is_err(),
|
||||
"{bad} 应解析失败或被拒"
|
||||
);
|
||||
for bad in [
|
||||
"query:这不是SQL",
|
||||
"query:SELECT FROM",
|
||||
"query:@#$%",
|
||||
"query:1+1",
|
||||
] {
|
||||
assert!(parse_source(bad).is_err(), "{bad} 应解析失败或被拒");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -733,8 +733,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn writes_affect_cache_false_for_read_only_statements() {
|
||||
for sql in ["SELECT 1", "EXPLAIN SELECT * FROM posts", "SELECT id FROM posts WHERE id = 1"]
|
||||
{
|
||||
for sql in [
|
||||
"SELECT 1",
|
||||
"EXPLAIN SELECT * FROM posts",
|
||||
"SELECT id FROM posts WHERE id = 1",
|
||||
] {
|
||||
assert!(
|
||||
!writes_affect_cache(&parse(sql)),
|
||||
"只读语句不应触发兜底失效:{sql:?}"
|
||||
|
||||
@ -63,7 +63,10 @@ fn physics_macros() -> &'static [(&'static str, MacroDefinition)] {
|
||||
r"\braket",
|
||||
MacroDefinition::StaticStr(r"\langle #1 | #2 \rangle"),
|
||||
),
|
||||
(r"\expval", MacroDefinition::StaticStr(r"\langle #1 \rangle")),
|
||||
(
|
||||
r"\expval",
|
||||
MacroDefinition::StaticStr(r"\langle #1 \rangle"),
|
||||
),
|
||||
// 向量 / 范数 / 绝对值(自动缩放定界符)
|
||||
(r"\abs", MacroDefinition::StaticStr(r"\left| #1 \right|")),
|
||||
(r"\norm", MacroDefinition::StaticStr(r"\left\| #1 \right\|")),
|
||||
@ -104,7 +107,6 @@ fn display_settings() -> Settings {
|
||||
};
|
||||
inject_physics_macros(&mut s);
|
||||
s
|
||||
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@ -136,7 +138,10 @@ fn expand_chem(tex: &str) -> String {
|
||||
while i < bytes.len() {
|
||||
// 匹配 `\ce{` 或 `\pu{`
|
||||
if bytes[i] == b'\\' && i + 3 < bytes.len() {
|
||||
let (is_ce, is_pu) = (bytes[i + 1] == b'c' && bytes[i + 2] == b'e', bytes[i + 1] == b'p' && bytes[i + 2] == b'u');
|
||||
let (is_ce, is_pu) = (
|
||||
bytes[i + 1] == b'c' && bytes[i + 2] == b'e',
|
||||
bytes[i + 1] == b'p' && bytes[i + 2] == b'u',
|
||||
);
|
||||
let brace_at = if is_ce {
|
||||
Some(i + 3)
|
||||
} else if is_pu {
|
||||
|
||||
@ -988,7 +988,11 @@ console.log(1)
|
||||
fn render_markdown_runnable_block_bun_canonical() {
|
||||
// bun 自身是 canonical(不是别名),runnable 块以 bun 执行。
|
||||
let result = render_markdown_enhanced("```bun runnable\nconsole.log('hi')\n```");
|
||||
assert!(result.html.contains(r#"data-lang="bun""#), "got: {}", result.html);
|
||||
assert!(
|
||||
result.html.contains(r#"data-lang="bun""#),
|
||||
"got: {}",
|
||||
result.html
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1065,18 +1069,23 @@ console.log(1)
|
||||
let result = render_markdown_enhanced("正文[^a]\n\n[^a]: 脚注内容\n");
|
||||
// 脚注引用:上标 + 锚点跳转 + role 语义
|
||||
assert!(
|
||||
result.html.contains(r#"<sup class="fn-ref" id="fnref:a-1">"#),
|
||||
result
|
||||
.html
|
||||
.contains(r#"<sup class="fn-ref" id="fnref:a-1">"#),
|
||||
"脚注引用上标应含正确 id, got: {}",
|
||||
result.html
|
||||
);
|
||||
assert!(
|
||||
result.html.contains(r##"href="#fn:a""##) && result.html.contains(r#"role="doc-noteref""#),
|
||||
result.html.contains(r##"href="#fn:a""##)
|
||||
&& result.html.contains(r#"role="doc-noteref""#),
|
||||
"引用链接应指向定义并带 noteref 角色, got: {}",
|
||||
result.html
|
||||
);
|
||||
// 脚注定义:<aside> + role + aria-labelledby
|
||||
assert!(
|
||||
result.html.contains(r#"<aside class="footnote-definition" id="fn:a""#)
|
||||
result
|
||||
.html
|
||||
.contains(r#"<aside class="footnote-definition" id="fn:a""#)
|
||||
&& result.html.contains(r#"role="doc-footnote""#),
|
||||
"定义应为 aside + doc-footnote 角色, got: {}",
|
||||
result.html
|
||||
@ -1129,8 +1138,12 @@ console.log(1)
|
||||
result.html
|
||||
);
|
||||
assert!(
|
||||
result.html.contains("↩<sup class=\"fn-backref-num\">2</sup>")
|
||||
&& result.html.contains("↩<sup class=\"fn-backref-num\">3</sup>"),
|
||||
result
|
||||
.html
|
||||
.contains("↩<sup class=\"fn-backref-num\">2</sup>")
|
||||
&& result
|
||||
.html
|
||||
.contains("↩<sup class=\"fn-backref-num\">3</sup>"),
|
||||
"第 2、3 个 back-link 应带数字上标, got: {}",
|
||||
result.html
|
||||
);
|
||||
@ -1147,9 +1160,8 @@ console.log(1)
|
||||
#[test]
|
||||
fn render_markdown_footnote_numbering_order() {
|
||||
// 多个不同脚注:编号按 label 首次出现顺序分配(1、2、3…),与定义位置无关。
|
||||
let result = render_markdown_enhanced(
|
||||
"先引用第二个[^b],再第一个[^a]\n\n[^b]: B注\n\n[^a]: A注\n",
|
||||
);
|
||||
let result =
|
||||
render_markdown_enhanced("先引用第二个[^b],再第一个[^a]\n\n[^b]: B注\n\n[^a]: A注\n");
|
||||
// b 先在正文被引用 → 编号 1;a 后被引用 → 编号 2。
|
||||
// 分别断言各自的引用链接块(由 id 唯一定位)。
|
||||
// b 的引用:id=fnref:b-1,aria-label=脚注 1
|
||||
@ -1266,7 +1278,12 @@ console.log(1)
|
||||
fn footnote_id_deterministic() {
|
||||
// 同一 label 多次调用必产生同一 id(ref↔def 双向一致的前提)。
|
||||
for label in ["a", "my note", "参考文献", "a!b@c#"] {
|
||||
assert_eq!(footnote_id(label), footnote_id(label), "label {:?} 不确定", label);
|
||||
assert_eq!(
|
||||
footnote_id(label),
|
||||
footnote_id(label),
|
||||
"label {:?} 不确定",
|
||||
label
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1275,8 +1292,18 @@ console.log(1)
|
||||
// 产出的 id 不得含破坏 HTML 属性或 URL 的字符:" ' < > & 空格。
|
||||
for label in ["a\"b", "x'y", "a<b>", "c&d", "e f", "a!@#$%^&*()b"] {
|
||||
let id = footnote_id(label);
|
||||
assert!(!id.contains('"'), "id {:?} 含双引号 (label {:?})", id, label);
|
||||
assert!(!id.contains('\''), "id {:?} 含单引号 (label {:?})", id, label);
|
||||
assert!(
|
||||
!id.contains('"'),
|
||||
"id {:?} 含双引号 (label {:?})",
|
||||
id,
|
||||
label
|
||||
);
|
||||
assert!(
|
||||
!id.contains('\''),
|
||||
"id {:?} 含单引号 (label {:?})",
|
||||
id,
|
||||
label
|
||||
);
|
||||
assert!(!id.contains('<'), "id {:?} 含 < (label {:?})", id, label);
|
||||
assert!(!id.contains('>'), "id {:?} 含 > (label {:?})", id, label);
|
||||
assert!(!id.contains('&'), "id {:?} 含 & (label {:?})", id, label);
|
||||
@ -1325,7 +1352,9 @@ console.log(1)
|
||||
fn render_markdown_sqrt_and_matrix_preserves_svg() {
|
||||
// \sqrt 与 \begin{pmatrix} 等 LaTeX 渲染需依赖 KaTeX 产生的 SVG 根号线与矩阵括号/竖线,
|
||||
// 验证经过 sanitizer clean_html 后 <svg> 与 <path> 不会被误丢。
|
||||
let result = render_markdown_enhanced("$$\\sqrt{\\pi} + \\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}$$");
|
||||
let result = render_markdown_enhanced(
|
||||
"$$\\sqrt{\\pi} + \\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}$$",
|
||||
);
|
||||
assert!(
|
||||
result.html.contains("<svg"),
|
||||
"KaTeX 根号/矩阵渲染的 <svg> 应保留, got: {}",
|
||||
|
||||
@ -51,16 +51,8 @@ enum Field {
|
||||
Nodes(Vec<Parsed>),
|
||||
}
|
||||
|
||||
impl Field {
|
||||
fn empty(&self) -> bool {
|
||||
match self {
|
||||
Field::Str(s) => s.is_empty(),
|
||||
Field::Nodes(v) => v.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析中间表示:字面字符串或节点。
|
||||
#[allow(clippy::large_enum_variant)] // 机械移植:镜像 mhchemParser 上游结构,Parsed 经 Vec 进堆,非栈热路径
|
||||
#[derive(Clone, Debug)]
|
||||
enum Parsed {
|
||||
S(String),
|
||||
@ -295,7 +287,7 @@ fn find_observe_groups(
|
||||
}
|
||||
|
||||
fn is_empty_pat(p: &Pat) -> bool {
|
||||
matches!(p, Pat::Lit("") )
|
||||
matches!(p, Pat::Lit(""))
|
||||
}
|
||||
|
||||
struct Span {
|
||||
@ -344,12 +336,19 @@ fn fancy_match(re: &Regex, input: &str) -> Option<MMatch> {
|
||||
let mval = if n_groups >= 2 {
|
||||
let mut v = Vec::with_capacity(n_groups);
|
||||
for gi in 1..=n_groups {
|
||||
v.push(caps.get(gi).map(|m| m.as_str().to_string()).unwrap_or_default());
|
||||
v.push(
|
||||
caps.get(gi)
|
||||
.map(|m| m.as_str().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
MVal::V(v)
|
||||
} else {
|
||||
// n_groups == 1 或 0:match[1] || match[0](空串视作未匹配)
|
||||
let g1 = caps.get(1).map(|m| m.as_str().to_string()).filter(|s| !s.is_empty());
|
||||
let g1 = caps
|
||||
.get(1)
|
||||
.map(|m| m.as_str().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
MVal::S(g1.unwrap_or(whole.clone()))
|
||||
};
|
||||
Some(MMatch {
|
||||
@ -398,11 +397,11 @@ fn match_pattern(name: &str, input: &str) -> Option<MMatch> {
|
||||
"x" => re_match(re!("^x")),
|
||||
"x$" => re_match(re!("^x$")),
|
||||
"i$" => re_match(re!("^i$")),
|
||||
"letters" => re_match(&*LETTERS_RE),
|
||||
"\\greek" => re_match(&*GREEK_RE),
|
||||
"letters" => re_match(&LETTERS_RE),
|
||||
"\\greek" => re_match(&GREEK_RE),
|
||||
"one lowercase latin letter $" => re_match(re!("^(?:([a-z])(?:$|[^a-zA-Z]))$")),
|
||||
"$one lowercase latin letter$ $" => re_match(re!("^\\$(?:([a-z])(?:$|[^a-zA-Z]))\\$$")),
|
||||
"one lowercase greek letter $" => re_match(&*ONE_GREEK_RE),
|
||||
"one lowercase greek letter $" => re_match(&ONE_GREEK_RE),
|
||||
"digits" => re_match(re!("^[0-9]+")),
|
||||
"-9.,9" => re_match(re!("^[+\\-]?(?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+))")),
|
||||
"-9.,9 no missing 0" => re_match(re!("^[+\\-]?[0-9]+(?:[.,][0-9]+)?")),
|
||||
@ -516,7 +515,11 @@ fn pat_enumber(input: &str) -> Option<MMatch> {
|
||||
}
|
||||
let mut v = Vec::with_capacity(6);
|
||||
for gi in 1..=6 {
|
||||
v.push(caps.get(gi).map(|m| m.as_str().to_string()).unwrap_or_default());
|
||||
v.push(
|
||||
caps.get(gi)
|
||||
.map(|m| m.as_str().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
Some(MMatch {
|
||||
m: MVal::V(v),
|
||||
@ -532,9 +535,16 @@ fn pat_state_of_aggregation(input: &str) -> Option<MMatch> {
|
||||
Pat::Re(re!("^\\([a-z]{1,3}(?=[\\),])")),
|
||||
Pat::Lit(""),
|
||||
Pat::Lit(")"),
|
||||
None, None, None, None, false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)?;
|
||||
if re!("^($|[\\s,;\\)\\]\\}])").is_match(a.remainder.as_str()).ok()? {
|
||||
if re!("^($|[\\s,;\\)\\]\\}])")
|
||||
.is_match(a.remainder.as_str())
|
||||
.ok()?
|
||||
{
|
||||
return Some(a);
|
||||
}
|
||||
let re2 = re!("^(?:\\((?:\\\\ca\\s?)?\\$[amothc]\\$\\))");
|
||||
@ -558,7 +568,18 @@ fn pat_amount(input: &str) -> Option<MMatch> {
|
||||
});
|
||||
}
|
||||
}
|
||||
let a = fg(input, Pat::Lit(""), Pat::Lit("$"), Pat::Lit("$"), Pat::Lit(""), None, None, None, None, false)?;
|
||||
let a = fg(
|
||||
input,
|
||||
Pat::Lit(""),
|
||||
Pat::Lit("$"),
|
||||
Pat::Lit("$"),
|
||||
Pat::Lit(""),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)?;
|
||||
let re2 = re!("^\\$(?:\\(?[+\\-]?(?:[0-9]*[a-z]?[+\\-])?[0-9]*[a-z](?:[+\\-][0-9]*[a-z]?)?\\)?|\\+|-)\\$$");
|
||||
let inner = mval_str(&a.m);
|
||||
let inner_len = inner.len();
|
||||
@ -596,6 +617,7 @@ fn mval_str(m: &MVal) -> String {
|
||||
// 状态机主循环(对应 `_mhchemParser.go`)
|
||||
// =========================================================================
|
||||
|
||||
#[allow(clippy::large_enum_variant)] // 同上:Out 为动作返回值,瞬态使用
|
||||
enum Out {
|
||||
None,
|
||||
One(Parsed),
|
||||
@ -622,8 +644,10 @@ fn go(input: &str, machine: &str) -> Vec<Parsed> {
|
||||
|
||||
let transitions = transitions_for(machine);
|
||||
let mut state = String::from("0");
|
||||
let mut buffer = Buffer::default();
|
||||
buffer.parenthesis_level = 0;
|
||||
let mut buffer = Buffer {
|
||||
parenthesis_level: 0,
|
||||
..Default::default()
|
||||
};
|
||||
let mut output: Vec<Parsed> = Vec::new();
|
||||
let mut last_input: Option<String> = None;
|
||||
let mut watchdog = 10i32;
|
||||
@ -753,7 +777,7 @@ fn generic_action(buf: &mut Buffer, m: &MVal, opt: &Option<String>, type_: &str)
|
||||
if let (Some(a), MVal::V(v)) = (opt, m) {
|
||||
Out::One(Parsed::N(NodeData {
|
||||
type_: a.clone(),
|
||||
p1: Some(Field::Str(v.get(0).cloned().unwrap_or_default())),
|
||||
p1: Some(Field::Str(v.first().cloned().unwrap_or_default())),
|
||||
p2: Some(Field::Str(v.get(1).cloned().unwrap_or_default())),
|
||||
..Default::default()
|
||||
}))
|
||||
@ -808,9 +832,15 @@ fn generic_action(buf: &mut Buffer, m: &MVal, opt: &Option<String>, type_: &str)
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
let mut n1 = caps.get(1).map(|x| x.as_str().to_string()).unwrap_or_default();
|
||||
let mut n1 = caps
|
||||
.get(1)
|
||||
.map(|x| x.as_str().to_string())
|
||||
.unwrap_or_default();
|
||||
n1 = n1.replace('$', "");
|
||||
let n2 = caps.get(2).map(|x| x.as_str().to_string()).unwrap_or_default();
|
||||
let n2 = caps
|
||||
.get(2)
|
||||
.map(|x| x.as_str().to_string())
|
||||
.unwrap_or_default();
|
||||
ret.push(Parsed::N(NodeData {
|
||||
type_: "frac".into(),
|
||||
p1: Some(Field::Str(n1)),
|
||||
@ -921,8 +951,14 @@ fn texify_go2(buf: &NodeData) -> String {
|
||||
res.push_str(&format!("^{{\\hphantom{{{}}}}}_{{\\hphantom{{{}}}}}", b, p));
|
||||
res.push_str("\\mkern-1.5mu");
|
||||
res.push_str("{\\vphantom{A}}");
|
||||
res.push_str(&format!("^{{\\smash[t]{{\\vphantom{{2}}}}\\llap{{{}}}}}", b));
|
||||
res.push_str(&format!("_{{\\vphantom{{2}}\\llap{{\\smash[t]{{{}}}}}}}", p));
|
||||
res.push_str(&format!(
|
||||
"^{{\\smash[t]{{\\vphantom{{2}}}}\\llap{{{}}}}}",
|
||||
b
|
||||
));
|
||||
res.push_str(&format!(
|
||||
"_{{\\vphantom{{2}}\\llap{{\\smash[t]{{{}}}}}}}",
|
||||
p
|
||||
));
|
||||
}
|
||||
// o
|
||||
if !o.is_empty() {
|
||||
@ -969,7 +1005,10 @@ fn texify_go2(buf: &NodeData) -> String {
|
||||
}
|
||||
res
|
||||
}
|
||||
"rm" => format!("\\mathrm{{{}}}", buf.p1.as_ref().map(field_str).unwrap_or_default()),
|
||||
"rm" => format!(
|
||||
"\\mathrm{{{}}}",
|
||||
buf.p1.as_ref().map(field_str).unwrap_or_default()
|
||||
),
|
||||
"text" => {
|
||||
let mut p1 = buf.p1.as_ref().map(field_str).unwrap_or_default();
|
||||
if p1.contains('^') || p1.contains('_') {
|
||||
@ -979,9 +1018,18 @@ fn texify_go2(buf: &NodeData) -> String {
|
||||
format!("\\text{{{}}}", p1)
|
||||
}
|
||||
}
|
||||
"roman numeral" => format!("\\mathrm{{{}}}", buf.p1.as_ref().map(field_str).unwrap_or_default()),
|
||||
"state of aggregation" => format!("\\mskip2mu {}", buf.p1.as_ref().map(go_inner).unwrap_or_default()),
|
||||
"state of aggregation subscript" => format!("\\mskip1mu {}", buf.p1.as_ref().map(go_inner).unwrap_or_default()),
|
||||
"roman numeral" => format!(
|
||||
"\\mathrm{{{}}}",
|
||||
buf.p1.as_ref().map(field_str).unwrap_or_default()
|
||||
),
|
||||
"state of aggregation" => format!(
|
||||
"\\mskip2mu {}",
|
||||
buf.p1.as_ref().map(go_inner).unwrap_or_default()
|
||||
),
|
||||
"state of aggregation subscript" => format!(
|
||||
"\\mskip1mu {}",
|
||||
buf.p1.as_ref().map(go_inner).unwrap_or_default()
|
||||
),
|
||||
"bond" => get_bond(buf.kind_.as_deref().unwrap_or("")),
|
||||
"frac" => {
|
||||
let c = format!(
|
||||
@ -1066,9 +1114,18 @@ fn texify_go2(buf: &NodeData) -> String {
|
||||
"pu-space-2" => "\\mkern3mu ".to_string(),
|
||||
"1000 separator" => "\\mkern2mu ".to_string(),
|
||||
"commaDecimal" => "{,}".to_string(),
|
||||
"comma enumeration L" => format!("{{{}}}\\mkern6mu ", buf.p1.as_ref().map(field_str).unwrap_or_default()),
|
||||
"comma enumeration M" => format!("{{{}}}\\mkern3mu ", buf.p1.as_ref().map(field_str).unwrap_or_default()),
|
||||
"comma enumeration S" => format!("{{{}}}\\mkern1mu ", buf.p1.as_ref().map(field_str).unwrap_or_default()),
|
||||
"comma enumeration L" => format!(
|
||||
"{{{}}}\\mkern6mu ",
|
||||
buf.p1.as_ref().map(field_str).unwrap_or_default()
|
||||
),
|
||||
"comma enumeration M" => format!(
|
||||
"{{{}}}\\mkern3mu ",
|
||||
buf.p1.as_ref().map(field_str).unwrap_or_default()
|
||||
),
|
||||
"comma enumeration S" => format!(
|
||||
"{{{}}}\\mkern1mu ",
|
||||
buf.p1.as_ref().map(field_str).unwrap_or_default()
|
||||
),
|
||||
"hyphen" => "\\text{-}".to_string(),
|
||||
"addition compound" => "\\,{\\cdot}\\,".to_string(),
|
||||
"electron dot" => "\\mkern1mu \\bullet\\mkern1mu ".to_string(),
|
||||
@ -1195,7 +1252,10 @@ mod tests {
|
||||
fn ce_gas_arrow_becomes_uparrow() {
|
||||
// 行尾 ^ 气体符号转译后应含 uparrow(消解原行尾 ^ 解析错误)。
|
||||
let tex = ce("CaCO3 ->[\\Delta] CaO + CO2 ^");
|
||||
assert!(tex.contains("uparrow") || tex.contains("rightarrow"), "气体/反应符号: {tex}");
|
||||
assert!(
|
||||
tex.contains("uparrow") || tex.contains("rightarrow"),
|
||||
"气体/反应符号: {tex}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -2,10 +2,6 @@
|
||||
// 数据机械移植自 mhchemParser 4.2.2 的 `stateMachines`。
|
||||
// (被 include 进 mhchem.rs 中部,故不能用 `//!` 内部文档注释。)
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Task 构造器 ──────────────────────────────────────────────────────────
|
||||
|
||||
fn task(acts: &[(&str, Option<&str>)], next: Option<&str>, revisit: bool, cont: bool) -> Task {
|
||||
@ -1220,7 +1216,7 @@ fn text_action(buf: &mut Buffer, type_: &str) -> Option<Out> {
|
||||
}
|
||||
}
|
||||
|
||||
fn pq_action(buf: &mut Buffer, m: &MVal, type_: &str) -> Option<Out> {
|
||||
fn pq_action(_buf: &mut Buffer, m: &MVal, type_: &str) -> Option<Out> {
|
||||
match type_ {
|
||||
"state of aggregation" => Some(Out::One(Parsed::N(NodeData {
|
||||
type_: "state of aggregation subscript".into(),
|
||||
@ -1307,7 +1303,7 @@ fn pu_action(buf: &mut Buffer, m: &MVal, opt: &Option<String>, type_: &str) -> O
|
||||
MVal::S(s) => vec![s.clone()],
|
||||
};
|
||||
let mut ret: Vec<Parsed> = Vec::new();
|
||||
let g0 = v.get(0).map(|s| s.as_str()).unwrap_or("");
|
||||
let g0 = v.first().map(|s| s.as_str()).unwrap_or("");
|
||||
if g0 == "+-" || g0 == "+/-" {
|
||||
ret.push(Parsed::S("\\pm ".into()));
|
||||
} else if !g0.is_empty() {
|
||||
@ -1352,7 +1348,7 @@ fn pu_action(buf: &mut Buffer, m: &MVal, opt: &Option<String>, type_: &str) -> O
|
||||
MVal::S(s) => vec![s.clone()],
|
||||
};
|
||||
let mut ret: Vec<Parsed> = Vec::new();
|
||||
let g0 = v.get(0).map(|s| s.as_str()).unwrap_or("");
|
||||
let g0 = v.first().map(|s| s.as_str()).unwrap_or("");
|
||||
if g0 == "+-" || g0 == "+/-" {
|
||||
ret.push(Parsed::S("\\pm ".into()));
|
||||
} else if !g0.is_empty() {
|
||||
|
||||
@ -23,11 +23,11 @@ pub mod image;
|
||||
/// KaTeX 服务端数学公式渲染(server-only)。
|
||||
#[cfg(feature = "server")]
|
||||
pub mod katex;
|
||||
/// Markdown 渲染与 HTML 清理。
|
||||
pub mod markdown;
|
||||
/// mhchem 化学公式转译器(\ce/\pu → LaTeX,server-only)。
|
||||
#[cfg(feature = "server")]
|
||||
pub mod mhchem;
|
||||
/// Markdown 渲染与 HTML 清理。
|
||||
pub mod markdown;
|
||||
/// 文章 CRUD 相关接口。
|
||||
pub mod posts;
|
||||
/// 限流工具。
|
||||
|
||||
@ -537,14 +537,32 @@ mod tests {
|
||||
fn katex_svg_and_path_preserved() {
|
||||
let katex_html = crate::api::katex::render_display("\\sqrt{\\pi}");
|
||||
let cleaned = clean_html(&katex_html);
|
||||
assert!(cleaned.contains("<svg"), "clean_html should preserve <svg> for KaTeX sqrt");
|
||||
assert!(cleaned.contains("<path"), "clean_html should preserve <path> for KaTeX sqrt");
|
||||
assert!(cleaned.contains("d="), "clean_html should preserve d attribute on <path>");
|
||||
assert!(cleaned.contains("viewBox=") || cleaned.contains("viewbox="), "clean_html should preserve viewBox attribute on <svg>");
|
||||
assert!(
|
||||
cleaned.contains("<svg"),
|
||||
"clean_html should preserve <svg> for KaTeX sqrt"
|
||||
);
|
||||
assert!(
|
||||
cleaned.contains("<path"),
|
||||
"clean_html should preserve <path> for KaTeX sqrt"
|
||||
);
|
||||
assert!(
|
||||
cleaned.contains("d="),
|
||||
"clean_html should preserve d attribute on <path>"
|
||||
);
|
||||
assert!(
|
||||
cleaned.contains("viewBox=") || cleaned.contains("viewbox="),
|
||||
"clean_html should preserve viewBox attribute on <svg>"
|
||||
);
|
||||
|
||||
let comment_cleaned = clean_comment_html(&katex_html);
|
||||
assert!(comment_cleaned.contains("<svg"), "clean_comment_html should preserve <svg>");
|
||||
assert!(comment_cleaned.contains("<path"), "clean_comment_html should preserve <path>");
|
||||
assert!(
|
||||
comment_cleaned.contains("<svg"),
|
||||
"clean_comment_html should preserve <svg>"
|
||||
);
|
||||
assert!(
|
||||
comment_cleaned.contains("<path"),
|
||||
"clean_comment_html should preserve <path>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -812,9 +830,15 @@ mod tests {
|
||||
fn clean_html_removes_script_tag_and_content() {
|
||||
// script 在 CLEAN_CONTENT_TAGS:标签连同内容一起移除(而非转义后保留)。
|
||||
let result = clean_html("<p>hi</p><script>alert(1)</script><p>bye</p>");
|
||||
assert!(!result.contains("script"), "script 标签应被完全移除: {result}");
|
||||
assert!(
|
||||
!result.contains("script"),
|
||||
"script 标签应被完全移除: {result}"
|
||||
);
|
||||
assert!(!result.contains("alert"), "script 内容应被清除: {result}");
|
||||
assert!(result.contains("hi") && result.contains("bye"), "周围内容应保留: {result}");
|
||||
assert!(
|
||||
result.contains("hi") && result.contains("bye"),
|
||||
"周围内容应保留: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -822,7 +846,10 @@ mod tests {
|
||||
// style 也走 CLEAN_CONTENT_TAGS:CSS 注入(expression()、@import)随内容一起移除。
|
||||
let result = clean_html("<style>body{background:url(javascript:alert(1))}</style><p>x</p>");
|
||||
assert!(!result.contains("style"), "style 标签应被移除: {result}");
|
||||
assert!(!result.contains("javascript"), "style 内危险内容应被清除: {result}");
|
||||
assert!(
|
||||
!result.contains("javascript"),
|
||||
"style 内危险内容应被清除: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -840,8 +867,14 @@ mod tests {
|
||||
// svg 在白名单中(配合 KaTeX 渲染),但非白名单属性(如 src / onerror / onload)会被剥离
|
||||
let svg_input = r#"<svg src="javascript:alert(1)" onload="alert(2)"></svg>"#;
|
||||
let svg_result = clean_html(svg_input);
|
||||
assert!(!svg_result.contains("javascript"), "svg 上的非白名单属性/javascript 应被剥离: {svg_result}");
|
||||
assert!(!svg_result.contains("onload"), "svg 上的 onload 事件处理器应被剥离: {svg_result}");
|
||||
assert!(
|
||||
!svg_result.contains("javascript"),
|
||||
"svg 上的非白名单属性/javascript 应被剥离: {svg_result}"
|
||||
);
|
||||
assert!(
|
||||
!svg_result.contains("onload"),
|
||||
"svg 上的 onload 事件处理器应被剥离: {svg_result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -883,7 +916,11 @@ mod tests {
|
||||
// 即便 allow_data_uri=true,is_safe_data_uri 也只放行图片类型;
|
||||
// data:text/html(可执行脚本)永远拒绝。直接测内部函数锁定该不变量。
|
||||
let schemes = DEFAULT_ALLOWED_SCHEMES.clone();
|
||||
assert!(!is_safe_url("data:text/html,<script>alert(1)</script>", &schemes, true));
|
||||
assert!(!is_safe_url(
|
||||
"data:text/html,<script>alert(1)</script>",
|
||||
&schemes,
|
||||
true
|
||||
));
|
||||
assert!(!is_safe_url(
|
||||
"data:application/javascript,alert(1)",
|
||||
&schemes,
|
||||
|
||||
@ -11,15 +11,12 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// SQL 补全用 schema 数据,由 `get_db_schema` server function 填充。
|
||||
// 暂未被消费(Task 5 的 get_db_schema 会用到),先 allow dead_code 避免 -D warnings 阻断 clippy。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
|
||||
pub struct SqlSchema {
|
||||
pub tables: Vec<SqlTable>,
|
||||
}
|
||||
|
||||
/// 单张表的补全数据:表名 + 列名列表。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct SqlTable {
|
||||
pub name: String,
|
||||
|
||||
@ -190,9 +190,7 @@ pub fn SearchIconLink() -> Element {
|
||||
view_box: "0 -960 960 960",
|
||||
width: "24px",
|
||||
fill: "currentColor",
|
||||
path {
|
||||
d: "M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z",
|
||||
}
|
||||
path { d: "M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +179,11 @@ pub fn PostContent(content_html: String) -> Element {
|
||||
.map(|el| el.class_list().contains("is-theme-transitioning"))
|
||||
.unwrap_or(false);
|
||||
if !transitioning {
|
||||
invoke_optional_global(&window, "__initMermaid", &[".post-content".into(), theme_str.into()]);
|
||||
invoke_optional_global(
|
||||
&window,
|
||||
"__initMermaid",
|
||||
&[".post-content".into(), theme_str.into()],
|
||||
);
|
||||
}
|
||||
|
||||
// lightbox 改由 Dioxus.toml 全局 <script src> 加载(不再 include_str!)。
|
||||
|
||||
@ -23,7 +23,10 @@ pub mod server {
|
||||
("Kotlin", include_str!("../syntaxes/Kotlin.sublime-syntax")),
|
||||
("Swift", include_str!("../syntaxes/Swift.sublime-syntax")),
|
||||
("TSX", include_str!("../syntaxes/TSX.sublime-syntax")),
|
||||
("TypeScript", include_str!("../syntaxes/TypeScript.sublime-syntax")),
|
||||
(
|
||||
"TypeScript",
|
||||
include_str!("../syntaxes/TypeScript.sublime-syntax"),
|
||||
),
|
||||
("Vue", include_str!("../syntaxes/Vue.sublime-syntax")),
|
||||
("Zig", include_str!("../syntaxes/Zig.sublime-syntax")),
|
||||
];
|
||||
|
||||
@ -614,7 +614,10 @@ mod tests {
|
||||
|
||||
// 隔离不变量:这些配置若被误删,容器隔离会悄悄失效。
|
||||
// 单独断言而非隐含在端到端测试里,确保回归可见。
|
||||
assert_eq!(host_config.cap_drop.as_deref(), Some(&["ALL".to_string()][..]));
|
||||
assert_eq!(
|
||||
host_config.cap_drop.as_deref(),
|
||||
Some(&["ALL".to_string()][..])
|
||||
);
|
||||
assert_eq!(
|
||||
host_config.security_opt.as_deref(),
|
||||
Some(&["no-new-privileges".to_string()][..])
|
||||
@ -673,10 +676,16 @@ mod tests {
|
||||
output_bytes: 1024,
|
||||
allow_network: false,
|
||||
};
|
||||
assert_eq!(build_host_config(&base).network_mode.as_deref(), Some("none"));
|
||||
assert_eq!(
|
||||
build_host_config(&base).network_mode.as_deref(),
|
||||
Some("none")
|
||||
);
|
||||
let mut net = base;
|
||||
net.allow_network = true;
|
||||
assert_eq!(build_host_config(&net).network_mode.as_deref(), Some("bridge"));
|
||||
assert_eq!(
|
||||
build_host_config(&net).network_mode.as_deref(),
|
||||
Some("bridge")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -335,7 +335,9 @@ fn RebuildCacheBar() -> Element {
|
||||
}
|
||||
// 重建结果消息:绝对定位到按钮行正下方,脱离文档流,不影响布局高度。
|
||||
if let Some(msg) = rebuild_result() {
|
||||
div { class: "absolute top-full right-0 mt-1 text-xs text-paper-secondary whitespace-pre-line", "{msg}" }
|
||||
div { class: "absolute top-full right-0 mt-1 text-xs text-paper-secondary whitespace-pre-line",
|
||||
"{msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ fn route_cache_dir(route: &str) -> Option<PathBuf> {
|
||||
path.push(seg);
|
||||
}
|
||||
if path.as_path() == std::path::Path::new(SSR_CACHE_ROOT) {
|
||||
None // 根路由("/")单独由 invalidate_ssr_home 处理
|
||||
None // 根路由("/")无目录段;首页缓存由 invalidate_ssr_all_public 覆盖删除
|
||||
} else {
|
||||
Some(path)
|
||||
}
|
||||
@ -72,19 +72,6 @@ pub fn invalidate_ssr_route(route: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 失效首页 SSR 缓存(路由 `/`,落盘在 `static/index/`)。
|
||||
///
|
||||
/// 通常 [`invalidate_ssr_all_public`] 已覆盖首页;本函数留作只需刷新首页的细粒度场景。
|
||||
#[allow(dead_code)]
|
||||
pub fn invalidate_ssr_home() {
|
||||
let dir = PathBuf::from(SSR_CACHE_ROOT).join("index");
|
||||
match std::fs::remove_dir_all(&dir) {
|
||||
Ok(()) => tracing::debug!("SSR 首页缓存已删除"),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => tracing::warn!(error = %e, "删除 SSR 首页缓存失败"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 失效所有公开页 SSR 缓存(删除 `static/` 下除 `.well-known`、`admin` 外的全部)。
|
||||
///
|
||||
/// 用于批量重建等影响面广的写入。保留 `.well-known`(浏览器/PWA 元数据,
|
||||
@ -112,7 +99,7 @@ pub fn invalidate_ssr_all_public() {
|
||||
/// 原子递增并返回新的全局世代号。
|
||||
///
|
||||
/// 仅作可观测性用途(注入 `X-SSR-Generation` 响应头)。实际 SSR 缓存失效由
|
||||
/// [`invalidate_ssr_route`] / [`invalidate_ssr_home`] 物理删文件完成。
|
||||
/// [`invalidate_ssr_route`] 物理删文件完成(首页由 [`invalidate_ssr_all_public`] 覆盖)。
|
||||
pub fn bump_global_generation() -> u64 {
|
||||
let new = GLOBAL_GENERATION
|
||||
.fetch_add(1, Ordering::SeqCst)
|
||||
@ -164,7 +151,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn route_cache_dir_none_for_root() {
|
||||
// 根路由 "/" 无目录段,由 invalidate_ssr_home 单独处理。
|
||||
// 根路由 "/" 无目录段(首页缓存由 invalidate_ssr_all_public 覆盖)。
|
||||
assert!(route_cache_dir("/").is_none());
|
||||
assert!(route_cache_dir("").is_none());
|
||||
}
|
||||
@ -195,6 +182,5 @@ mod tests {
|
||||
// (若真实环境已有 static/,本测试不会误删——read_dir 对每个条目单独删,
|
||||
// 这里仅验证根缺失分支。)
|
||||
invalidate_ssr_all_public();
|
||||
invalidate_ssr_home();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user