tree_sitter_cli/
playground.rs1use std::{
2 borrow::Cow,
3 env, fs,
4 net::TcpListener,
5 path::{Path, PathBuf},
6 str::{self, FromStr as _},
7};
8
9use anyhow::{anyhow, Context, Result};
10use tiny_http::{Header, Response, Server};
11
12use super::wasm;
13
14macro_rules! optional_resource {
15 ($name:tt, $path:tt) => {
16 #[cfg(TREE_SITTER_EMBED_WASM_BINDING)]
17 fn $name(tree_sitter_dir: Option<&Path>) -> Cow<'static, [u8]> {
18 if let Some(tree_sitter_dir) = tree_sitter_dir {
19 Cow::Owned(fs::read(tree_sitter_dir.join($path)).unwrap())
20 } else {
21 Cow::Borrowed(include_bytes!(concat!("../../", $path)))
22 }
23 }
24
25 #[cfg(not(TREE_SITTER_EMBED_WASM_BINDING))]
26 fn $name(tree_sitter_dir: Option<&Path>) -> Cow<'static, [u8]> {
27 if let Some(tree_sitter_dir) = tree_sitter_dir {
28 Cow::Owned(fs::read(tree_sitter_dir.join($path)).unwrap())
29 } else {
30 Cow::Borrowed(&[])
31 }
32 }
33 };
34}
35
36optional_resource!(get_playground_js, "docs/src/assets/js/playground.js");
37optional_resource!(get_lib_js, "lib/binding_web/tree-sitter.js");
38optional_resource!(get_lib_wasm, "lib/binding_web/tree-sitter.wasm");
39
40fn get_main_html(tree_sitter_dir: Option<&Path>) -> Cow<'static, [u8]> {
41 tree_sitter_dir.map_or(
42 Cow::Borrowed(include_bytes!("playground.html")),
43 |tree_sitter_dir| {
44 Cow::Owned(fs::read(tree_sitter_dir.join("cli/src/playground.html")).unwrap())
45 },
46 )
47}
48
49pub fn serve(grammar_path: &Path, open_in_browser: bool) -> Result<()> {
50 let server = get_server()?;
51 let (grammar_name, language_wasm) = wasm::load_language_wasm_file(grammar_path)?;
52 let url = format!("http://{}", server.server_addr());
53 println!("Started playground on: {url}");
54 if open_in_browser && webbrowser::open(&url).is_err() {
55 eprintln!("Failed to open '{url}' in a web browser");
56 }
57
58 let tree_sitter_dir = env::var("TREE_SITTER_BASE_DIR").map(PathBuf::from).ok();
59 let main_html = str::from_utf8(&get_main_html(tree_sitter_dir.as_deref()))
60 .unwrap()
61 .replace("THE_LANGUAGE_NAME", &grammar_name)
62 .into_bytes();
63 let playground_js = get_playground_js(tree_sitter_dir.as_deref());
64 let lib_js = get_lib_js(tree_sitter_dir.as_deref());
65 let lib_wasm = get_lib_wasm(tree_sitter_dir.as_deref());
66
67 let html_header = Header::from_str("Content-Type: text/html").unwrap();
68 let js_header = Header::from_str("Content-Type: application/javascript").unwrap();
69 let wasm_header = Header::from_str("Content-Type: application/wasm").unwrap();
70
71 for request in server.incoming_requests() {
72 let res = match request.url() {
73 "/" => response(&main_html, &html_header),
74 "/tree-sitter-parser.wasm" => response(&language_wasm, &wasm_header),
75 "/playground.js" => {
76 if playground_js.is_empty() {
77 redirect("https://tree-sitter.github.io/tree-sitter/assets/js/playground.js")
78 } else {
79 response(&playground_js, &js_header)
80 }
81 }
82 "/tree-sitter.js" => {
83 if lib_js.is_empty() {
84 redirect("https://tree-sitter.github.io/tree-sitter.js")
85 } else {
86 response(&lib_js, &js_header)
87 }
88 }
89 "/tree-sitter.wasm" => {
90 if lib_wasm.is_empty() {
91 redirect("https://tree-sitter.github.io/tree-sitter.wasm")
92 } else {
93 response(&lib_wasm, &wasm_header)
94 }
95 }
96 _ => response(b"Not found", &html_header).with_status_code(404),
97 };
98 request
99 .respond(res)
100 .with_context(|| "Failed to write HTTP response")?;
101 }
102
103 Ok(())
104}
105
106fn redirect(url: &str) -> Response<&[u8]> {
107 Response::empty(302)
108 .with_data("".as_bytes(), Some(0))
109 .with_header(Header::from_bytes("Location", url.as_bytes()).unwrap())
110}
111
112fn response<'a>(data: &'a [u8], header: &Header) -> Response<&'a [u8]> {
113 Response::empty(200)
114 .with_data(data, Some(data.len()))
115 .with_header(header.clone())
116}
117
118fn get_server() -> Result<Server> {
119 let addr = env::var("TREE_SITTER_PLAYGROUND_ADDR").unwrap_or_else(|_| "127.0.0.1".to_owned());
120 let port = env::var("TREE_SITTER_PLAYGROUND_PORT")
121 .map(|v| {
122 v.parse::<u16>()
123 .with_context(|| "Invalid port specification")
124 })
125 .ok();
126 let listener = match port {
127 Some(port) => {
128 bind_to(&addr, port?).with_context(|| "Failed to bind to the specified port")?
129 }
130 None => get_listener_on_available_port(&addr)
131 .with_context(|| "Failed to find a free port to bind to it")?,
132 };
133 let server =
134 Server::from_listener(listener, None).map_err(|_| anyhow!("Failed to start web server"))?;
135 Ok(server)
136}
137
138fn get_listener_on_available_port(addr: &str) -> Option<TcpListener> {
139 (8000..12000).find_map(|port| bind_to(addr, port))
140}
141
142fn bind_to(addr: &str, port: u16) -> Option<TcpListener> {
143 TcpListener::bind(format!("{addr}:{port}")).ok()
144}