tree_sitter_cli/
playground.rs

1use 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 log::{error, info};
11use tiny_http::{Header, Response, Server};
12
13use super::wasm;
14
15macro_rules! optional_resource {
16    ($name:tt, $path:tt) => {
17        #[cfg(TREE_SITTER_EMBED_WASM_BINDING)]
18        fn $name(tree_sitter_dir: Option<&Path>) -> Cow<'static, [u8]> {
19            if let Some(tree_sitter_dir) = tree_sitter_dir {
20                Cow::Owned(fs::read(tree_sitter_dir.join($path)).unwrap())
21            } else {
22                Cow::Borrowed(include_bytes!(concat!("../../../", $path)))
23            }
24        }
25
26        #[cfg(not(TREE_SITTER_EMBED_WASM_BINDING))]
27        fn $name(tree_sitter_dir: Option<&Path>) -> Cow<'static, [u8]> {
28            if let Some(tree_sitter_dir) = tree_sitter_dir {
29                Cow::Owned(fs::read(tree_sitter_dir.join($path)).unwrap())
30            } else {
31                Cow::Borrowed(&[])
32            }
33        }
34    };
35}
36
37optional_resource!(get_playground_js, "docs/src/assets/js/playground.js");
38optional_resource!(get_lib_js, "lib/binding_web/web-tree-sitter.js");
39optional_resource!(get_lib_wasm, "lib/binding_web/web-tree-sitter.wasm");
40
41fn get_main_html(tree_sitter_dir: Option<&Path>) -> Cow<'static, [u8]> {
42    tree_sitter_dir.map_or(
43        Cow::Borrowed(include_bytes!("playground.html")),
44        |tree_sitter_dir| {
45            Cow::Owned(fs::read(tree_sitter_dir.join("crates/cli/src/playground.html")).unwrap())
46        },
47    )
48}
49
50pub fn export(grammar_path: &Path, export_path: &Path) -> Result<()> {
51    let (grammar_name, language_wasm) = wasm::load_language_wasm_file(grammar_path)?;
52
53    fs::create_dir_all(export_path).with_context(|| {
54        format!(
55            "Failed to create export directory: {}",
56            export_path.display()
57        )
58    })?;
59
60    let tree_sitter_dir = env::var("TREE_SITTER_BASE_DIR").map(PathBuf::from).ok();
61
62    let playground_js = get_playground_js(tree_sitter_dir.as_deref());
63    let lib_js = get_lib_js(tree_sitter_dir.as_deref());
64    let lib_wasm = get_lib_wasm(tree_sitter_dir.as_deref());
65
66    let has_local_playground_js = !playground_js.is_empty();
67    let has_local_lib_js = !lib_js.is_empty();
68    let has_local_lib_wasm = !lib_wasm.is_empty();
69
70    let mut main_html = str::from_utf8(&get_main_html(tree_sitter_dir.as_deref()))
71        .unwrap()
72        .replace("THE_LANGUAGE_NAME", &grammar_name);
73
74    if !has_local_playground_js {
75        main_html = main_html.replace(
76            r#"<script type="module" src="playground.js"></script>"#,
77            r#"<script type="module" src="https://tree-sitter.github.io/tree-sitter/assets/js/playground.js"></script>"#
78        );
79    }
80    if !has_local_lib_js {
81        main_html = main_html.replace(
82            "import * as TreeSitter from './web-tree-sitter.js';",
83            "import * as TreeSitter from 'https://tree-sitter.github.io/web-tree-sitter.js';",
84        );
85    }
86
87    fs::write(export_path.join("index.html"), main_html.as_bytes())
88        .with_context(|| "Failed to write index.html")?;
89
90    fs::write(export_path.join("tree-sitter-parser.wasm"), language_wasm)
91        .with_context(|| "Failed to write parser wasm file")?;
92
93    if has_local_playground_js {
94        fs::write(export_path.join("playground.js"), playground_js)
95            .with_context(|| "Failed to write playground.js")?;
96    }
97
98    if has_local_lib_js {
99        fs::write(export_path.join("web-tree-sitter.js"), lib_js)
100            .with_context(|| "Failed to write web-tree-sitter.js")?;
101    }
102
103    if has_local_lib_wasm {
104        fs::write(export_path.join("web-tree-sitter.wasm"), lib_wasm)
105            .with_context(|| "Failed to write web-tree-sitter.wasm")?;
106    }
107
108    println!(
109        "Exported playground to {}",
110        export_path.canonicalize()?.display()
111    );
112
113    Ok(())
114}
115
116pub fn serve(grammar_path: &Path, open_in_browser: bool) -> Result<()> {
117    let server = get_server()?;
118    let (grammar_name, language_wasm) = wasm::load_language_wasm_file(grammar_path)?;
119    let url = format!("http://{}", server.server_addr());
120    info!("Started playground on: {url}");
121    if open_in_browser && webbrowser::open(&url).is_err() {
122        error!("Failed to open '{url}' in a web browser");
123    }
124
125    let tree_sitter_dir = env::var("TREE_SITTER_BASE_DIR").map(PathBuf::from).ok();
126    let main_html = str::from_utf8(&get_main_html(tree_sitter_dir.as_deref()))
127        .unwrap()
128        .replace("THE_LANGUAGE_NAME", &grammar_name)
129        .into_bytes();
130    let playground_js = get_playground_js(tree_sitter_dir.as_deref());
131    let lib_js = get_lib_js(tree_sitter_dir.as_deref());
132    let lib_wasm = get_lib_wasm(tree_sitter_dir.as_deref());
133
134    let html_header = Header::from_str("Content-Type: text/html").unwrap();
135    let js_header = Header::from_str("Content-Type: application/javascript").unwrap();
136    let wasm_header = Header::from_str("Content-Type: application/wasm").unwrap();
137
138    for request in server.incoming_requests() {
139        let res = match request.url() {
140            "/" => response(&main_html, &html_header),
141            "/tree-sitter-parser.wasm" => response(&language_wasm, &wasm_header),
142            "/playground.js" => {
143                if playground_js.is_empty() {
144                    redirect("https://tree-sitter.github.io/tree-sitter/assets/js/playground.js")
145                } else {
146                    response(&playground_js, &js_header)
147                }
148            }
149            "/web-tree-sitter.js" => {
150                if lib_js.is_empty() {
151                    redirect("https://tree-sitter.github.io/web-tree-sitter.js")
152                } else {
153                    response(&lib_js, &js_header)
154                }
155            }
156            "/web-tree-sitter.wasm" => {
157                if lib_wasm.is_empty() {
158                    redirect("https://tree-sitter.github.io/web-tree-sitter.wasm")
159                } else {
160                    response(&lib_wasm, &wasm_header)
161                }
162            }
163            _ => response(b"Not found", &html_header).with_status_code(404),
164        };
165        request
166            .respond(res)
167            .with_context(|| "Failed to write HTTP response")?;
168    }
169
170    Ok(())
171}
172
173fn redirect(url: &str) -> Response<&[u8]> {
174    Response::empty(302)
175        .with_data("".as_bytes(), Some(0))
176        .with_header(Header::from_bytes("Location", url.as_bytes()).unwrap())
177}
178
179fn response<'a>(data: &'a [u8], header: &Header) -> Response<&'a [u8]> {
180    Response::empty(200)
181        .with_data(data, Some(data.len()))
182        .with_header(header.clone())
183}
184
185fn get_server() -> Result<Server> {
186    let addr = env::var("TREE_SITTER_PLAYGROUND_ADDR").unwrap_or_else(|_| "127.0.0.1".to_owned());
187    let port = env::var("TREE_SITTER_PLAYGROUND_PORT")
188        .map(|v| {
189            v.parse::<u16>()
190                .with_context(|| "Invalid port specification")
191        })
192        .ok();
193    let listener = match port {
194        Some(port) => {
195            bind_to(&addr, port?).with_context(|| "Failed to bind to the specified port")?
196        }
197        None => get_listener_on_available_port(&addr)
198            .with_context(|| "Failed to find a free port to bind to it")?,
199    };
200    let server =
201        Server::from_listener(listener, None).map_err(|_| anyhow!("Failed to start web server"))?;
202    Ok(server)
203}
204
205fn get_listener_on_available_port(addr: &str) -> Option<TcpListener> {
206    (8000..12000).find_map(|port| bind_to(addr, port))
207}
208
209fn bind_to(addr: &str, port: u16) -> Option<TcpListener> {
210    TcpListener::bind(format!("{addr}:{port}")).ok()
211}