speccoder_runtime/
documents.rs1use std::path::Path;
7
8use pulldown_cmark::{Event, Options, Parser, html};
9use serde::Serialize;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
13pub struct Document {
14 pub name: String,
16 pub path: String,
18 pub html: String,
20}
21
22#[must_use]
26pub fn render(markdown: &str) -> String {
27 let options =
28 Options::ENABLE_TABLES | Options::ENABLE_TASKLISTS | Options::ENABLE_STRIKETHROUGH;
29 let events = Parser::new_ext(markdown, options).map(|e| match e {
30 Event::Html(t) | Event::InlineHtml(t) => Event::Text(t),
31 other => other,
32 });
33 let mut out = String::new();
34 html::push_html(&mut out, events);
35 out
36}
37
38#[must_use]
41pub fn sources(path: &Path, feature: Option<&str>) -> Vec<(String, String, String)> {
42 let format = crate::format::at(path);
43 let Some(snap) = format.open(path, feature) else {
44 return Vec::new();
45 };
46 let Some(f) = snap.feature.as_ref() else {
47 return Vec::new();
48 };
49 let root = Path::new(&snap.root);
50 [
51 "spec",
52 "plan",
53 "tasks",
54 "research",
55 "data-model",
56 "quickstart",
57 ]
58 .iter()
59 .filter_map(|name| {
60 let rel = format!("{}/{name}.md", f.feature.dir);
61 std::fs::read_to_string(root.join(&rel))
62 .ok()
63 .map(|text| ((*name).to_owned(), rel, text))
64 })
65 .collect()
66}
67
68#[must_use]
70pub fn of_feature(path: &Path, feature: Option<&str>) -> Vec<Document> {
71 sources(path, feature)
72 .into_iter()
73 .map(|(name, path, text)| Document {
74 name,
75 path,
76 html: render(&text),
77 })
78 .collect()
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn a_file_cannot_style_or_script_the_page() {
87 let html = render(
88 "# Hi {style=position:fixed;inset:0 #top .x}\n\n<script>alert(1)</script>\n\n<b onclick=x>b</b>\n",
89 );
90 assert!(html.starts_with("<h1>Hi {style="), "{html}");
92 assert!(!html.contains("<h1 "), "{html}");
93 assert!(!html.contains("<script"), "{html}");
94 assert!(!html.contains("<b "), "{html}");
95 }
96
97 #[test]
98 fn markdown_renders_and_raw_html_is_text() {
99 let html = render("# Spec\n\n- [ ] T001 a\n\n<script>alert(1)</script>\n\nok <b>x</b>\n");
100 assert!(html.contains("<h1>Spec</h1>"), "{html}");
101 assert!(html.contains("type=\"checkbox\""), "{html}");
102 assert!(
103 !html.contains("<script>") && html.contains("<script>"),
104 "{html}"
105 );
106 assert!(!html.contains("<b>"), "{html}");
107 }
108}