use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use dioxus::prelude::*;
use crate::interop;
use crate::types::{CursorPosition, LivePreviewVariant, Mode, ParsedDoc, Selection};
use crop::Rope;
static NEXT_ROOT_ID: AtomicU64 = AtomicU64::new(1);
pub(crate) fn make_instance_n() -> u64 {
NEXT_ROOT_ID.fetch_add(1, Ordering::Relaxed)
}
#[derive(Clone, Copy)]
pub struct MarkdownContext {
pub mode: Signal<Mode>,
pub is_mode_controlled: bool,
pub on_mode_change: Option<EventHandler<Mode>>,
pub raw_content: Signal<Rc<RefCell<Rope>>>,
pub is_value_controlled: bool,
pub on_value_change: Option<EventHandler<String>>,
pub parsed_doc: Memo<Rc<ParsedDoc>>,
pub is_editor_scrolling: Signal<bool>,
pub is_preview_scrolling: Signal<bool>,
pub instance_n: u64,
pub editor_mount: Signal<Option<Rc<MountedData>>>,
pub disabled: bool,
pub trigger_parse: Callback<()>,
pub live_preview_variant: Signal<LivePreviewVariant>,
pub highlight_class_prefix: Signal<String>,
pub show_code_line_numbers: bool,
pub show_code_language: bool,
pub show_editor_line_numbers: bool,
}
impl MarkdownContext {
pub fn current_mode(&self) -> Mode {
*self.mode.read()
}
pub fn handle_mode_change(&mut self, mode: Mode) {
if self.is_mode_controlled {
if let Some(handler) = &self.on_mode_change {
handler.call(mode);
}
} else {
let mut mode_signal = self.mode;
mode_signal.set(mode);
}
}
pub fn handle_value_change(&self, value: String) {
if let Some(handler) = &self.on_value_change {
handler.call(value.clone());
}
*self.raw_content.read().borrow_mut() = Rope::from(value);
}
pub fn raw_value(&self) -> String {
self.raw_content.read().borrow().to_string()
}
pub fn editor_id(&self) -> String {
format!("nox-md-{}-editor", self.instance_n)
}
pub fn preview_id(&self) -> String {
format!("nox-md-{}-preview", self.instance_n)
}
pub fn source_panel_id(&self) -> String {
format!("nox-md-{}-source", self.instance_n)
}
pub fn read_panel_id(&self) -> String {
format!("nox-md-{}-read", self.instance_n)
}
pub fn inline_editor_id(&self) -> String {
format!("nox-md-{}-inline", self.instance_n)
}
pub fn gutter_id(&self) -> String {
format!("nox-md-{}-gutter", self.instance_n)
}
}
#[derive(Clone, Copy)]
pub struct CursorContext {
pub cursor_position: Signal<CursorPosition>,
pub selection: Signal<Option<Selection>>,
pub preedit: Signal<Option<String>>,
}
pub fn use_markdown_context() -> MarkdownContext {
use_context::<MarkdownContext>()
}
pub fn use_cursor_context() -> Option<CursorContext> {
try_use_context::<CursorContext>()
}
pub(crate) async fn read_cursor_and_selection(
editor_id: &str,
text: &str,
) -> Option<(CursorPosition, Option<Selection>)> {
let js = interop::caret_adapter().read_textarea_selection_js(editor_id);
let mut eval = interop::start_eval(&js);
let arr = interop::recv_vec_u64(&mut eval).await?;
let start_utf16 = *arr.first()? as usize;
let end_utf16 = *arr.get(1)? as usize;
let start_byte = utf16_to_byte_index_ctx(text, start_utf16).unwrap_or(0);
let end_byte = utf16_to_byte_index_ctx(text, end_utf16).unwrap_or(start_byte);
let pos = CursorPosition {
offset: start_byte,
line: 0, column: 0,
};
let sel = if start_byte == end_byte {
None
} else {
Some(Selection {
anchor: start_byte,
head: end_byte,
})
};
Some((pos, sel))
}
fn utf16_to_byte_index_ctx(s: &str, utf16_idx: usize) -> Option<usize> {
let mut utf16_count = 0usize;
for (byte_idx, ch) in s.char_indices() {
if utf16_count == utf16_idx {
return Some(byte_idx);
}
utf16_count += ch.len_utf16();
}
if utf16_count == utf16_idx {
Some(s.len())
} else {
None
}
}
pub(crate) fn escape_js(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'\'' => out.push_str("\\'"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\u{2028}' => out.push_str("\\u2028"),
'\u{2029}' => out.push_str("\\u2029"),
_ => out.push(ch),
}
}
out
}
pub(crate) fn handle_insert_text_js(editor_id: &str, text: &str) -> String {
let text_escaped = escape_js(text);
let text_utf16_len: usize = text
.chars()
.map(|c| if (c as u32) > 0xFFFF { 2 } else { 1 })
.sum();
format!(
r#"(function() {{
var el = document.getElementById('{editor_id}');
if (!el) return null;
var start = el.selectionStart;
var end = el.selectionEnd;
el.value = el.value.substring(0, start) + '{text}' + el.value.substring(end);
el.setSelectionRange(start + {text_len}, start + {text_len});
el.dispatchEvent(new Event('input', {{ bubbles: true }}));
return null;
}})();"#,
editor_id = editor_id,
text = text_escaped,
text_len = text_utf16_len,
)
}
pub(crate) fn handle_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 null;
}})();"#,
editor_id = editor_id,
prefix = prefix_escaped,
suffix = suffix_escaped,
prefix_len = prefix.len(),
)
}
pub(crate) fn handle_set_content_js(editor_id: &str, text: &str) -> String {
let text_escaped = escape_js(text);
format!(
r#"(function() {{
var el = document.getElementById('{editor_id}');
if (!el) return null;
el.value = '{text}';
el.dispatchEvent(new Event('input', {{ bubbles: true }}));
return null;
}})();"#,
editor_id = editor_id,
text = text_escaped,
)
}
pub(crate) fn handle_set_content_with_cursor_js(
editor_id: &str,
text: &str,
cursor_byte_offset: usize,
) -> String {
let text_escaped = escape_js(text);
let clamped = cursor_byte_offset.min(text.len());
let cursor_utf16: usize = text[..clamped].encode_utf16().count();
format!(
r#"(function() {{
var el = document.getElementById('{editor_id}');
if (!el) return null;
el.value = '{text}';
el.setSelectionRange({cursor}, {cursor});
el.dispatchEvent(new Event('input', {{ bubbles: true }}));
el.focus();
return null;
}})();"#,
editor_id = editor_id,
text = text_escaped,
cursor = cursor_utf16,
)
}
#[derive(Clone, Copy)]
pub struct MarkdownHandle {
instance_n: u64,
editor_mount: Signal<Option<Rc<MountedData>>>,
}
impl MarkdownHandle {
fn editor_id(&self) -> String {
format!("nox-md-{}-editor", self.instance_n)
}
pub async fn insert_text(&self, text: &str) {
interop::eval_void(&handle_insert_text_js(&self.editor_id(), text)).await;
}
pub async fn wrap_selection(&self, prefix: &str, suffix: &str) {
interop::eval_void(&handle_wrap_selection_js(&self.editor_id(), prefix, suffix)).await;
}
pub async fn focus(&self) {
if let Some(node) = self.editor_mount.read().as_ref() {
let _ = node.set_focus(true).await;
}
}
pub async fn blur(&self) {
if let Some(node) = self.editor_mount.read().as_ref() {
let _ = node.set_focus(false).await;
}
}
pub async fn set_content(&self, text: &str) {
interop::eval_void(&handle_set_content_js(&self.editor_id(), text)).await;
}
}
pub fn use_markdown_handle() -> MarkdownHandle {
let ctx = use_context::<MarkdownContext>();
MarkdownHandle {
instance_n: ctx.instance_n,
editor_mount: ctx.editor_mount,
}
}