1use wasm_bindgen::prelude::*;
34
35use crate::core::{Element, Node, Render, RenderOptions};
36use crate::document::Document;
37use crate::seo::SeoBuilder;
38
39#[wasm_bindgen(js_name = WElement)]
49#[derive(Debug, Clone)]
50pub struct WElement(Element);
51
52#[wasm_bindgen(js_class = WElement)]
53impl WElement {
54 #[wasm_bindgen(constructor)]
56 #[must_use]
57 pub fn new(tag: &str) -> Self {
58 Self(Element::new(tag))
59 }
60
61 #[wasm_bindgen(js_name = setId)]
63 #[must_use]
64 pub fn set_id(self, id: &str) -> Self {
65 Self(self.0.set_id(id))
66 }
67
68 #[wasm_bindgen(js_name = addClass)]
70 #[must_use]
71 pub fn add_class(self, class_name: &str) -> Self {
72 Self(self.0.add_class(class_name))
73 }
74
75 #[wasm_bindgen(js_name = setStyle)]
77 #[must_use]
78 pub fn set_style(self, style: &str) -> Self {
79 Self(self.0.set_style(style))
80 }
81
82 #[must_use]
87 pub fn attr(self, key: &str, value: &str) -> Self {
88 Self(self.0.attr(key, value))
89 }
90
91 #[wasm_bindgen(js_name = trustedUrl)]
95 #[must_use]
96 pub fn trusted_url(self, key: &str, value: &str) -> Self {
97 Self(self.0.trusted_url(key, value))
98 }
99
100 #[wasm_bindgen(js_name = boolAttr)]
102 #[must_use]
103 pub fn bool_attr(self, key: &str) -> Self {
104 Self(self.0.bool_attr(key))
105 }
106
107 #[wasm_bindgen(js_name = dataAttr)]
109 #[must_use]
110 pub fn data_attr(self, key: &str, value: &str) -> Self {
111 Self(self.0.data_attr(key, value))
112 }
113
114 #[wasm_bindgen(js_name = ariaAttr)]
116 #[must_use]
117 pub fn aria_attr(self, key: &str, value: &str) -> Self {
118 Self(self.0.aria_attr(key, value))
119 }
120
121 #[must_use]
123 pub fn text(self, content: &str) -> Self {
124 Self(self.0.text(content))
125 }
126
127 #[wasm_bindgen(js_name = rawText)]
129 #[must_use]
130 pub fn raw_text(self, content: &str) -> Self {
131 Self(self.0.raw_text(content))
132 }
133
134 #[must_use]
136 pub fn child(self, child: WElement) -> Self {
137 Self(self.0.child(child.0))
138 }
139
140 #[wasm_bindgen(js_name = addClasses)]
142 #[must_use]
143 pub fn add_classes(self, classes: Vec<String>) -> Self {
144 Self(self.0.add_classes(classes))
145 }
146
147 #[wasm_bindgen(js_name = rawChild)]
151 #[must_use]
152 pub fn raw_child(self, markup: &str) -> Self {
153 Self(self.0.child(Node::raw(markup)))
154 }
155
156 #[wasm_bindgen(js_name = selfClosing)]
159 #[must_use]
160 pub fn self_closing(self, yes: bool) -> Self {
161 Self(self.0.self_closing(yes))
162 }
163
164 #[must_use]
166 pub fn comment(self, content: &str) -> Self {
167 Self(self.0.child(Node::comment(content)))
168 }
169
170 #[must_use]
172 pub fn render(&self) -> String {
173 self.0.render()
174 }
175
176 #[wasm_bindgen(js_name = renderPretty)]
178 #[must_use]
179 pub fn render_pretty(&self) -> String {
180 self.0.render_pretty()
181 }
182
183 #[wasm_bindgen(js_name = renderWithOptions)]
186 #[must_use]
187 pub fn render_with_options(&self, pretty: bool, indent: &str, xhtml: bool) -> String {
188 self.0.render_with(&options(pretty, indent, xhtml))
189 }
190
191 #[wasm_bindgen(js_name = renderBytes)]
193 #[must_use]
194 pub fn render_bytes(&self) -> Vec<u8> {
195 self.0.render().into_bytes()
196 }
197}
198
199fn options(pretty: bool, indent: &str, xhtml: bool) -> RenderOptions {
200 let base = if pretty {
201 RenderOptions::pretty()
202 } else {
203 RenderOptions::compact()
204 };
205 base.with_indent(indent).with_xhtml_self_closing(xhtml)
206}
207
208#[wasm_bindgen(js_name = WDocument)]
210#[derive(Debug, Clone)]
211pub struct WDocument(Document);
212
213#[wasm_bindgen(js_class = WDocument)]
214impl WDocument {
215 #[allow(clippy::needless_pass_by_value)]
219 #[wasm_bindgen(constructor)]
220 #[must_use]
221 pub fn new(lang: Option<String>) -> Self {
222 Self(Document::new(lang.as_deref()))
223 }
224
225 #[wasm_bindgen(js_name = addHead)]
227 #[must_use]
228 pub fn add_head(self, element: WElement) -> Self {
229 Self(self.0.head_children([element.0]))
230 }
231
232 #[wasm_bindgen(js_name = addBody)]
234 #[must_use]
235 pub fn add_body(self, element: WElement) -> Self {
236 Self(self.0.body_children([element.0]))
237 }
238
239 #[allow(clippy::needless_pass_by_value)]
243 #[wasm_bindgen(js_name = addSeo)]
244 #[must_use]
245 pub fn add_seo(self, seo: WSeo) -> Self {
246 Self(self.0.head_children(seo.0.build()))
247 }
248
249 #[must_use]
251 pub fn render(&self) -> String {
252 self.0.render()
253 }
254
255 #[wasm_bindgen(js_name = renderCompact)]
257 #[must_use]
258 pub fn render_compact(&self) -> String {
259 self.0.render_compact()
260 }
261
262 #[wasm_bindgen(js_name = renderWithIndent)]
264 #[must_use]
265 pub fn render_with_indent(&self, indent: &str) -> String {
266 self.0
267 .render_with(&RenderOptions::pretty().with_indent(indent))
268 }
269
270 #[wasm_bindgen(js_name = renderWithOptions)]
273 #[must_use]
274 pub fn render_with_options(&self, pretty: bool, indent: &str, xhtml: bool) -> String {
275 self.0.render_with(&options(pretty, indent, xhtml))
276 }
277}
278
279#[wasm_bindgen(js_name = WSeo)]
281#[derive(Debug, Clone)]
282pub struct WSeo(SeoBuilder);
283
284#[wasm_bindgen(js_class = WSeo)]
285impl WSeo {
286 #[wasm_bindgen(constructor)]
288 #[must_use]
289 pub fn new(title: &str, description: &str) -> Self {
290 Self(SeoBuilder::new(title, description))
291 }
292
293 #[must_use]
295 pub fn image(self, image_url: &str) -> Self {
296 Self(self.0.image(image_url))
297 }
298
299 #[must_use]
301 pub fn url(self, page_url: &str) -> Self {
302 Self(self.0.url(page_url))
303 }
304
305 #[wasm_bindgen(js_name = twitterSite)]
307 #[must_use]
308 pub fn twitter_site(self, site: &str) -> Self {
309 Self(self.0.twitter_site(site))
310 }
311
312 #[must_use]
314 pub fn render(&self) -> String {
315 self.0.build().iter().map(Render::render).collect()
316 }
317}
318
319#[wasm_bindgen]
322#[must_use]
323pub fn element(tag: &str) -> WElement {
324 WElement::new(tag)
325}
326
327#[wasm_bindgen(js_name = jsonLd)]
330#[must_use]
331pub fn json_ld(json: &str) -> WElement {
332 WElement(crate::seo::json_ld(json))
333}
334
335#[wasm_bindgen(js_name = escapeText)]
337#[must_use]
338pub fn escape_text(input: &str) -> String {
339 crate::core::escape::escape_text(input).into_owned()
340}
341
342#[wasm_bindgen]
344#[must_use]
345pub fn version() -> String {
346 env!("CARGO_PKG_VERSION").to_string()
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn trusted_url_skips_the_script_url_check() {
355 assert_eq!(
356 WElement::new("a")
357 .trusted_url("href", "javascript:print()")
358 .render(),
359 "<a href=\"javascript:print()\"></a>"
360 );
361 assert_eq!(
362 WElement::new("a").attr("href", "javascript:x").render(),
363 "<a href=\"#\"></a>"
364 );
365 }
366}