style: 格式化 Rust 与 TS 代码
This commit is contained in:
parent
17d596c512
commit
db74a96de1
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());
|
||||
|
||||
@ -21,10 +21,7 @@ const unescape = (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: '',
|
||||
});
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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: {}",
|
||||
|
||||
@ -287,7 +287,7 @@ fn find_observe_groups(
|
||||
}
|
||||
|
||||
fn is_empty_pat(p: &Pat) -> bool {
|
||||
matches!(p, Pat::Lit("") )
|
||||
matches!(p, Pat::Lit(""))
|
||||
}
|
||||
|
||||
struct Span {
|
||||
@ -336,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 {
|
||||
@ -508,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),
|
||||
@ -524,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]\\$\\))");
|
||||
@ -550,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();
|
||||
@ -803,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)),
|
||||
@ -916,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() {
|
||||
@ -964,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('_') {
|
||||
@ -974,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!(
|
||||
@ -1061,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(),
|
||||
@ -1190,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]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user