Skip to main content

yggdrasil/api/markdown/
code_block.rs

1//! 围栏代码块:runnable 标记解析与 `<pre><code>` 输出(syntect 高亮 / 可运行;mermaid 交给 [`super::mermaid`])。
2
3use std::fmt::Write as _;
4
5/// 解析围栏 info:识别 `runnable` 标记与可选 ResourceLimits JSON 覆盖。
6///
7/// 仅在「标记为 runnable 且语言受支持」时返回 `(lang, html-escaped overrides JSON)`,
8/// 供阅读器扫描 data-* 挂载运行器;其余情况返回 `None`。
9pub(super) fn runnable_attrs(info: &str) -> Option<(String, String)> {
10    let (lang, runnable, overrides) = crate::api::code_runner::languages::parse_fence_info(info);
11    if runnable && crate::api::code_runner::languages::is_supported_lang(&lang) {
12        // overrides 序列化为 JSON 后 HTML 转义,避免属性注入。
13        let ov_json = overrides
14            .map(|o| serde_json::to_string(&o).unwrap_or_default())
15            .unwrap_or_default();
16        let overrides_escaped = crate::utils::html::escape_html(&ov_json);
17        Some((lang, overrides_escaped))
18    } else {
19        None
20    }
21}
22
23/// 输出一个完整的围栏代码块 `<pre><code>…</code></pre>`。
24///
25/// `info` 为围栏 info 字符串,`runnable` 为 [`runnable_attrs`] 的结果,`source` 为原始源码。
26pub(super) fn push_code_block(
27    html: &mut String,
28    info: Option<&str>,
29    runnable: Option<(String, String)>,
30    source: &str,
31    diagrams: &mut super::mermaid::Diagrams,
32) {
33    // mermaid 代码块:服务端渲染成浅色、深色两份 SVG(先写占位符,sanitize 后替换);
34    // 渲染失败时降级为转义后的源码。
35    let is_mermaid = info
36        .map(|l| l.split_whitespace().next() == Some("mermaid"))
37        .unwrap_or(false);
38    if is_mermaid {
39        diagrams.push(html, source);
40        return;
41    }
42    // 使用 syntect 对代码块进行服务端语法高亮。围栏信息可能带修饰
43    //(`python runnable {...}`),只取首个语言 token。
44    let highlight_lang = info.and_then(|lang| lang.split_whitespace().next());
45    let highlighted = crate::highlight::server::highlight_code(source, highlight_lang);
46    // 可运行代码块:在 <pre> 上挂 data-runnable / data-lang / data-overrides / data-source,
47    // 阅读者(post_content.rs)客户端扫描这些标记原地挂载 CodeRunner 组件。
48    // data-source 为 HTML 转义后的原始源码,供阅读器无损提取(避免反解高亮 HTML)。
49    if let Some((lang, overrides_escaped)) = runnable {
50        let source_escaped = crate::utils::html::escape_html(source);
51        let _ = write!(
52            html,
53            r#"<pre data-runnable="true" data-lang="{lang}" data-overrides="{overrides_escaped}" data-source="{source_escaped}"><code class="language-{lang}">"#
54        );
55    } else {
56        html.push_str("<pre><code");
57        if let Some(lang) = info {
58            // 围栏语言可能含 info 修饰(如 `python runnable {...}`),
59            // 高亮的 language-xxx 取纯语言 token(首个空白前)。
60            let clean_lang = lang.split_whitespace().next().unwrap_or("");
61            if !clean_lang.is_empty() {
62                let _ = write!(html, " class=\"language-{clean_lang}\"");
63            }
64        }
65        html.push('>');
66    }
67    html.push_str(&highlighted);
68    html.push_str("</code></pre>");
69}