use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(target_arch = "wasm32")]
use gloo_timers::callback::Timeout;
use dioxus::prelude::*;
use dioxus_core::use_drop;
pub(crate) static NEXT_VH_ID: AtomicU64 = AtomicU64::new(1);
use crate::context::{MarkdownContext, escape_js};
use crate::interop;
use crate::parser::parse_document;
use crate::types::{HeadingEntry, ParsedDoc};
#[cfg(test)]
pub(crate) fn compute_scroll_ratio(scroll_top: f64, scroll_height: f64, client_height: f64) -> f64 {
let max_scroll = scroll_height - client_height;
if max_scroll <= 0.0 {
return 0.0;
}
(scroll_top / max_scroll).clamp(0.0, 1.0)
}
pub(crate) fn wrap_selection_js(editor_id: &str, prefix: &str, suffix: &str) -> String {
let prefix_escaped = escape_js(prefix);
let suffix_escaped = escape_js(suffix);
format!(
r#"(function() {{
var el = document.getElementById('{editor_id}');
if (!el) return null;
var start = el.selectionStart;
var end = el.selectionEnd;
var selected = el.value.substring(start, end);
el.value = el.value.substring(0, start) + '{prefix}' + selected + '{suffix}' + el.value.substring(end);
el.setSelectionRange(start + {prefix_len}, end + {prefix_len});
el.dispatchEvent(new Event('input', {{ bubbles: true }}));
return el.value;
}})();"#,
editor_id = editor_id,
prefix = prefix_escaped,
suffix = suffix_escaped,
prefix_len = prefix.len(),
)
}
pub(crate) fn tab_indent_js(editor_id: &str, tab_size: u8) -> String {
let spaces = " ".repeat(tab_size as usize);
format!(
r#"(function() {{
var el = document.getElementById('{editor_id}');
if (!el) return null;
var start = el.selectionStart;
el.value = el.value.substring(0, start) + '{spaces}' + el.value.substring(start);
el.setSelectionRange(start + {tab_size}, start + {tab_size});
el.dispatchEvent(new Event('input', {{ bubbles: true }}));
return el.value;
}})();"#,
editor_id = editor_id,
spaces = spaces,
tab_size = tab_size,
)
}
pub(crate) fn select_all_children_js(element_id: &str) -> String {
format!(
r#"(function() {{
var el = document.getElementById('{element_id}');
if (!el) return;
var sel = window.getSelection();
sel.removeAllRanges();
var range = document.createRange();
range.selectNodeContents(el);
sel.addRange(range);
}})();"#,
element_id = element_id,
)
}
pub fn use_heading_index() -> Memo<Vec<HeadingEntry>> {
let ctx = use_context::<MarkdownContext>();
use_memo(move || (ctx.parsed_doc)().headings.clone())
}
pub(crate) async fn sync_editor_to_preview(editor_id: &str, preview_id: &str) {
let js = format!(
r#"
const ed = document.getElementById('{editor_id}');
const pr = document.getElementById('{preview_id}');
if (!ed || !pr) return null;
const maxEd = Math.max(0, ed.scrollHeight - ed.clientHeight);
const ratio = maxEd > 0 ? ed.scrollTop / maxEd : 0;
pr.scrollTop = ratio * Math.max(0, pr.scrollHeight - pr.clientHeight);
null
"#
);
interop::eval_void(&js).await;
}
pub(crate) async fn sync_preview_to_editor(editor_id: &str, preview_id: &str) {
let js = format!(
r#"
const pr = document.getElementById('{preview_id}');
const ed = document.getElementById('{editor_id}');
if (!ed || !pr) return null;
const maxPr = Math.max(0, pr.scrollHeight - pr.clientHeight);
const ratio = maxPr > 0 ? pr.scrollTop / maxPr : 0;
ed.scrollTop = ratio * Math.max(0, ed.scrollHeight - ed.clientHeight);
null
"#
);
interop::eval_void(&js).await;
}
pub(crate) async fn sync_gutter_scroll(editor_id: &str, gutter_id: &str) {
let js = format!(
r#"
const ed = document.getElementById('{editor_id}');
const gt = document.getElementById('{gutter_id}');
if (ed && gt) {{ gt.scrollTop = ed.scrollTop; }}
null
"#
);
interop::eval_void(&js).await;
}
pub fn use_viewport_height() -> Signal<f64> {
let mut height = use_signal(|| 0.0_f64);
let cleanup_id = use_hook(|| NEXT_VH_ID.fetch_add(1, Ordering::Relaxed));
use_effect(move || {
spawn(async move {
let mut ev = interop::start_eval(&format!(
r#"
const send = () => dioxus.send(
window.visualViewport ? window.visualViewport.height : window.innerHeight
);
send();
const target = window.visualViewport || window;
target.addEventListener('resize', send);
window.__nox_md_cleanup = window.__nox_md_cleanup || {{}};
window.__nox_md_cleanup['nox_md_vh_{cleanup_id}'] = function() {{
target.removeEventListener('resize', send);
}};
"#
));
while let Some(h) = interop::recv_f64(&mut ev).await {
height.set(h);
}
});
});
use_drop(move || {
let key = format!("nox_md_vh_{cleanup_id}");
spawn(async move {
interop::eval_void(&format!(
"if(window.__nox_md_cleanup && window.__nox_md_cleanup['{key}']){{window.__nox_md_cleanup['{key}']();delete window.__nox_md_cleanup['{key}'];}}"
))
.await;
});
});
height
}
pub fn use_debounced_parse(
raw_content: Signal<Rc<RefCell<crop::Rope>>>,
delay_ms: u32,
) -> (Signal<Rc<ParsedDoc>>, Callback<()>) {
let parsed = use_signal(|| {
Rc::new(ParsedDoc {
element: rsx! {},
headings: vec![],
front_matter: None,
blocks: vec![],
ast: vec![],
})
});
#[cfg(target_arch = "wasm32")]
let timer_handle: Rc<RefCell<Option<Timeout>>> = use_hook(|| Rc::new(RefCell::new(None)));
let trigger = {
#[cfg(target_arch = "wasm32")]
let timer_handle = timer_handle.clone();
Callback::new(move |_: ()| {
let content_rc = raw_content.read().clone();
let mut parsed = parsed;
#[cfg(target_arch = "wasm32")]
{
let timer_rc2 = timer_handle.clone();
let mut guard = timer_handle.borrow_mut();
let _ = guard.take();
*guard = Some(Timeout::new(delay_ms, move || {
let rope = content_rc.borrow().clone();
let doc = parse_document(&rope);
parsed.set(Rc::new(doc));
*timer_rc2.borrow_mut() = None;
}));
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = delay_ms;
let rope = content_rc.borrow().clone();
let doc = parse_document(&rope);
parsed.set(Rc::new(doc));
}
})
};
(parsed, trigger)
}