use std::collections::{HashMap, HashSet};
use html5ever::{ParseOpts, tendril::TendrilSink};
use iced::widget;
use markup5ever_rcdom::RcDom;
use crate::structs::{UpdateMsg, UpdateMsgKind};
pub struct MarkState {
pub(crate) dom: RcDom,
pub(crate) selection_state: HashMap<String, widget::text_editor::Content>,
pub(crate) dropdown_state: HashMap<usize, bool>,
}
impl MarkState {
#[must_use]
#[allow(clippy::missing_panics_doc)] pub fn with_html(input: &str) -> Self {
let dom = html5ever::parse_document(RcDom::default(), ParseOpts::default())
.from_utf8()
.read_from(&mut input.as_bytes())
.unwrap();
let mut selection_state = HashMap::new();
let mut dropdown_state = HashMap::new();
let mut dropdown_counter = 0;
find_state(
&dom.document,
&mut selection_state,
&mut dropdown_state,
&mut dropdown_counter,
false,
);
Self {
dom,
selection_state,
dropdown_state,
}
}
#[must_use]
#[cfg(feature = "markdown")]
pub fn with_html_and_markdown(input: &str) -> Self {
let html = comrak::markdown_to_html(
input,
&comrak::Options {
extension: comrak::options::Extension {
strikethrough: true,
cjk_friendly_emphasis: true,
tasklist: true,
superscript: true,
subscript: true,
underline: true,
table: true,
..Default::default()
},
parse: comrak::options::Parse::default(),
render: comrak::options::Render {
r#unsafe: true,
..Default::default()
},
},
);
Self::with_html(&html)
}
#[must_use]
#[cfg(feature = "markdown")]
pub fn with_markdown_only(input: &str) -> Self {
let mut out = String::new();
_ = comrak::html::escape(&mut out, input);
Self::with_html_and_markdown(&out)
}
pub fn update(&mut self, action: UpdateMsg) {
match action.kind {
UpdateMsgKind::TextEditor(code, action) => {
if !action.is_edit() {
if let Some(n) = self.selection_state.get_mut(&code) {
n.perform(action);
}
}
}
UpdateMsgKind::DetailsToggle(id, action) => {
self.dropdown_state.insert(id, action);
}
}
}
#[must_use]
pub fn find_image_links(&self) -> HashSet<String> {
let mut storage = HashSet::new();
find_image_links(&self.dom.document, &mut storage);
storage
}
}
impl Default for MarkState {
fn default() -> Self {
Self::with_html("")
}
}
fn find_state(
node: &markup5ever_rcdom::Node,
selection_state: &mut HashMap<String, widget::text_editor::Content>,
dropdown_state: &mut HashMap<usize, bool>,
dropdown_counter: &mut usize,
scan_text: bool,
) {
let borrow = node.children.borrow();
match &node.data {
markup5ever_rcdom::NodeData::Element { name, .. } if &name.local == "code" => {
for child in &*borrow {
find_state(
child,
selection_state,
dropdown_state,
dropdown_counter,
true,
);
}
}
markup5ever_rcdom::NodeData::Element { name, .. } if &name.local == "details" => {
dropdown_state.insert(*dropdown_counter, false);
*dropdown_counter += 1;
for child in &*borrow {
find_state(
child,
selection_state,
dropdown_state,
dropdown_counter,
false,
);
}
}
markup5ever_rcdom::NodeData::Text { contents } if scan_text => {
let contents = contents.borrow().to_string();
let v = widget::text_editor::Content::with_text(&contents);
selection_state.insert(contents.clone(), v);
}
_ => {
for child in &*borrow {
find_state(
child,
selection_state,
dropdown_state,
dropdown_counter,
scan_text,
);
}
}
}
}
fn find_image_links(node: &markup5ever_rcdom::Node, storage: &mut HashSet<String>) {
let borrow = node.children.borrow();
match &node.data {
markup5ever_rcdom::NodeData::Element { name, attrs, .. } if &name.local == "img" => {
let attrs = attrs.borrow();
if let Some(attr) = attrs.iter().find(|attr| &*attr.name.local == "src") {
let url = &*attr.value;
if !url.is_empty() {
storage.insert(url.to_owned());
}
}
}
_ => {
for child in &*borrow {
find_image_links(child, storage);
}
}
}
}