Description
Feature Request
We should improve the experience for executing js code. The current way is not very ergonomic, especially with all the escaping. e.g.
pub async fn get_visible_html_content(root_id: Option<&str>) -> Result<Vec<String>> {
let js = format!( r#"
function getVisibleText(rootElementId = null) {{
let rootElement = null;
if (rootElementId) {{
// If specific ID provided, use that element
rootElement = document.getElementById(rootElementId);
}} else if (document.body) {{
// Use body if available
rootElement = document.body;
}} else {{
// Fallback: find first non-head element
const allRootElements = document.querySelectorAll(':root > *');
for (const el of allRootElements) {{
if (el.tagName.toLowerCase() !== 'head') {{
rootElement = el;
break;
}}
}}
}}
if (!rootElement) {{
throw new Error('No suitable root element found');
}}
// Query elements within the specified root
const elements = rootElement.querySelectorAll('*');
const visibleElements = Array.from(elements).filter(el => {{
// Skip script and style elements entirely
if (el.tagName.toLowerCase() === 'script' ||
el.tagName.toLowerCase() === 'style' ||
el.tagName.toLowerCase() === 'noscript') {{
return false;
}}
// Skip elements with display:none or visibility:hidden
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') {{
return false;
}}
// Check if element is in viewport
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
}});
// Extract only text content from visible elements
const visibleText = visibleElements.map(el => {{
// Get direct text content of this element (excluding child elements)
let textContent = '';
for (let node of el.childNodes) {{
if (node.nodeType === Node.TEXT_NODE) {{
const trimmedText = node.textContent.trim();
if (trimmedText) {{
textContent += trimmedText + ' ';
}}
}}
}}
return textContent.trim();
}}).filter(text => text !== '');
return visibleText;
}}
const visibleText = getVisibleText({});
return visibleText;
"#,
root_id.map(|e| format!("\"{e}\"")).unwrap_or(String::new())
);
let eval = document::eval(js.as_str());
serde_json::from_value(eval.await?)
}
A better way would be to use macros with the existing manganis system:
pub async fn get_visible_html_content(root_id: Option<&str>) -> Result<Vec<String>> {
document::module_eval!(
"path/to/asset.js", // asset that is loaded as a module
"getVisibleText", // function to invoke inside the module
root_id // arguments
).await?
}
Expands to something like:
pub async fn get_visible_html_content(root_id: Option<&str>) -> Result<Vec<String>> {
{
const MODULE: Asset = asset!("path/to/asset.js");
let js = const_format::formatcp!(r#"
import getVisibleText from "{MODULE}";
let arg1 = await dioxus.recv();
return getVisibleText(arg1);
"#);
let eval = document::eval(js);
eval.send(root_id)?;
serde_json::from_value(eval.await?)
}.await?
}
path/to/asset.js
function getVisibleText(rootElementId = null) {
let rootElement = null;
if (rootElementId) {
// If specific ID provided, use that element
rootElement = document.getElementById(rootElementId);
} else if (document.body) {
// Use body if available
rootElement = document.body;
} else {
// Fallback: find first non-head element
const allRootElements = document.querySelectorAll(':root > *');
for (const el of allRootElements) {
if (el.tagName.toLowerCase() !== 'head') {
rootElement = el;
break;
}
}
}
if (!rootElement) {
throw new Error('No suitable root element found');
}
// Query elements within the specified root
const elements = rootElement.querySelectorAll('*');
const visibleElements = Array.from(elements).filter(el => {
// Skip script and style elements entirely
if (el.tagName.toLowerCase() === 'script' ||
el.tagName.toLowerCase() === 'style' ||
el.tagName.toLowerCase() === 'noscript') {
return false;
}
// Skip elements with display:none or visibility:hidden
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') {
return false;
}
// Check if element is in viewport
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
});
// Extract only text content from visible elements
const visibleText = visibleElements.map(el => {
// Get direct text content of this element (excluding child elements)
let textContent = '';
for (let node of el.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
const trimmedText = node.textContent.trim();
if (trimmedText) {
textContent += trimmedText + ' ';
}
}
}
return textContent.trim();
}).filter(text => text !== '');
return visibleText;
}
This new proposed alternative allows loading the js code once, no escaping, easy integration of modules in js files, auto generate send
/recv
, compile time validation (js is valid, function exists, and number of args are correct), and ide/syntax highlighting support since js files are used.
A future version could even support callbacks. e.g.
document::module_eval!(
"path/to/asset.js",
"functionName",
arg1,
callback!(arg2),
callback!(arg3)
).await?
For args with callback!
, the necessary wrapping code would be generated and use dioxus.send
, dioxus.recv
, eval.send
, eval.recv
for communication.
I am interested in implementing this if accepted.