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 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}