browser_use/tools/
markdown.rs

1use crate::error::{BrowserError, Result};
2use crate::tools::{Tool, ToolContext, ToolResult};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Parameters for getting markdown content (no parameters needed)
7#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
8pub struct GetMarkdownParams {}
9
10#[derive(Default)]
11pub struct GetMarkdownTool;
12
13impl Tool for GetMarkdownTool {
14    type Params = GetMarkdownParams;
15
16    fn name(&self) -> &str {
17        "get_markdown"
18    }
19
20    fn execute_typed(
21        &self,
22        _params: GetMarkdownParams,
23        context: &mut ToolContext,
24    ) -> Result<ToolResult> {
25        // Wait for network idle with a timeout (similar to TypeScript version)
26        // Since headless_chrome doesn't have a direct network idle wait,
27        // we'll add a small delay to let dynamic content load
28        std::thread::sleep(std::time::Duration::from_millis(1000));
29
30        // Load the JavaScript code for markdown conversion
31        let js_code = include_str!("convert_to_markdown.js");
32
33        // Execute the JavaScript to extract and convert content
34        let result = context
35            .session
36            .tab()
37            .evaluate(js_code, false)
38            .map_err(|e| BrowserError::EvaluationFailed(e.to_string()))?;
39
40        // Parse the result
41        let result_value = result
42            .value
43            .ok_or_else(|| BrowserError::ToolExecutionFailed {
44                tool: "get_markdown".to_string(),
45                reason: "No value returned from JavaScript".to_string(),
46            })?;
47
48        // The JavaScript returns a JSON string, so we need to parse it
49        let content_data: MarkdownContent = if let Some(json_str) = result_value.as_str() {
50            serde_json::from_str(json_str).map_err(|e| BrowserError::ToolExecutionFailed {
51                tool: "get_markdown".to_string(),
52                reason: format!("Failed to parse markdown content: {}", e),
53            })?
54        } else {
55            // If it's already an object, try to deserialize directly
56            serde_json::from_value(result_value).map_err(|e| BrowserError::ToolExecutionFailed {
57                tool: "get_markdown".to_string(),
58                reason: format!("Failed to deserialize markdown content: {}", e),
59            })?
60        };
61
62        // Combine title and content
63        let markdown = if !content_data.title.is_empty() {
64            format!("# {}\n\n{}", content_data.title, content_data.content)
65        } else {
66            content_data.content.clone()
67        };
68
69        Ok(ToolResult::success_with(serde_json::json!({
70            "markdown": markdown,
71            "title": content_data.title,
72            "url": content_data.url,
73            "length": markdown.len()
74        })))
75    }
76}
77
78/// Structure for markdown content returned from JavaScript
79#[derive(Debug, Serialize, Deserialize)]
80struct MarkdownContent {
81    title: String,
82    content: String,
83    url: String,
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_get_markdown_tool_name() {
92        let tool = GetMarkdownTool::default();
93        assert_eq!(tool.name(), "get_markdown");
94    }
95
96    #[test]
97    fn test_get_markdown_params_schema() {
98        let tool = GetMarkdownTool::default();
99        let schema = tool.parameters_schema();
100        assert!(schema.is_object());
101    }
102
103    #[test]
104    fn test_markdown_content_deserialization() {
105        let json = r#"{"title": "Test", "content": "Hello", "url": "https://example.com"}"#;
106        let content: MarkdownContent = serde_json::from_str(json).unwrap();
107        assert_eq!(content.title, "Test");
108        assert_eq!(content.content, "Hello");
109        assert_eq!(content.url, "https://example.com");
110    }
111}