use crate::browser_data_format_helper::{is_text_format, to_data_format};
use crate::interop::completion_helper::PromiseError;
use crate::interop::input_helper::{self, ReadableDataValueContent};
use crate::interop::JsObject;
use crate::storage::JsStorageFile;
use ferroui_base::input::platform::ClipboardError;
use ferroui_base::input::{DataFormat, LocalBoxFuture};
use ferroui_base::media::imaging::Bitmap;
use ferroui_base::platform::storage::IStorageItem;
use std::any::Any;
use std::rc::Rc;
pub(crate) trait IReadableDataItem {
fn formats(&self) -> Vec<String>;
fn try_get_value_async(&self, format: &str) -> LocalBoxFuture<Result<Option<ReadableDataValueContent>, PromiseError>>;
fn try_get_value(&self, format: &str) -> Option<ReadableDataValueContent>;
}
pub(crate) trait IReadableDataItems {
fn len(&self) -> usize;
fn get(&self, index: usize) -> Rc<dyn IReadableDataItem>;
}
pub(crate) struct JsReadableDataItem(JsObject);
impl IReadableDataItem for JsReadableDataItem {
fn formats(&self) -> Vec<String> {
input_helper::get_readable_data_item_formats(&self.0)
}
fn try_get_value_async(&self, format: &str) -> LocalBoxFuture<Result<Option<ReadableDataValueContent>, PromiseError>> {
let item = self.0.clone();
let format = format.to_string();
Box::pin(async move {
let value = input_helper::try_get_readable_data_item_value_async(&item, &format).await?;
Ok(value.as_ref().and_then(input_helper::get_readable_data_value))
})
}
fn try_get_value(&self, format: &str) -> Option<ReadableDataValueContent> {
input_helper::try_get_readable_data_item_value(&self.0, format)
.as_ref()
.and_then(input_helper::get_readable_data_value)
}
}
pub(crate) struct JsReadableDataItems(JsObject);
impl JsReadableDataItems {
pub(crate) fn new(items: JsObject) -> Rc<dyn IReadableDataItems> {
Rc::new(Self(items))
}
}
impl IReadableDataItems for JsReadableDataItems {
fn len(&self) -> usize {
input_helper::get_array_length(&self.0) as usize
}
fn get(&self, index: usize) -> Rc<dyn IReadableDataItem> {
Rc::new(JsReadableDataItem(input_helper::get_array_item(&self.0, index as u32)))
}
}
pub(crate) fn get_readable_item_formats(readable_data_item: &dyn IReadableDataItem) -> Vec<DataFormat> {
let format_strings = readable_data_item.formats();
let mut formats = Vec::with_capacity(format_strings.len() + 1);
let mut has_supported_image = false;
for format_string in &format_strings {
has_supported_image |= format_string == "image/png";
formats.push(to_data_format(format_string));
}
if has_supported_image {
formats.push(DataFormat::bitmap().into());
}
formats
}
pub(crate) fn try_get_value(
readable_data_value: Option<ReadableDataValueContent>,
format: &DataFormat,
) -> Result<Option<Rc<dyn Any>>, ClipboardError> {
let Some(data) = readable_data_value else {
return Ok(None);
};
let data = match data {
ReadableDataValueContent::File(item) => {
let file: Rc<dyn IStorageItem> = JsStorageFile::new(item);
if DataFormat::file() == *format {
return Ok(Some(Rc::new(file)));
}
return Ok(None);
}
data => data,
};
if DataFormat::text() == *format {
return Ok(match data {
ReadableDataValueContent::String(text) => Some(Rc::new(text)),
_ => None,
});
}
if DataFormat::file() == *format {
return Ok(None);
}
if DataFormat::bitmap() == *format {
return match data {
ReadableDataValueContent::Bytes(bytes) => match Bitmap::from_stream(&mut bytes.as_slice()) {
Ok(bitmap) => Ok(Some(Rc::new(Rc::new(bitmap)))),
Err(error) => Err(ClipboardError::other(error.to_string())),
},
_ => Ok(None),
};
}
if is_string_format(format) {
return Ok(match data {
ReadableDataValueContent::String(text) => Some(Rc::new(text)),
ReadableDataValueContent::Bytes(bytes) => Some(Rc::new(String::from_utf8_lossy(&bytes).into_owned())),
ReadableDataValueContent::File(_) => None,
});
}
Ok(match data {
ReadableDataValueContent::Bytes(bytes) => Some(Rc::new(Rc::<[u8]>::from(bytes))),
ReadableDataValueContent::String(text) => Some(Rc::new(Rc::<[u8]>::from(text.into_bytes()))),
ReadableDataValueContent::File(_) => None,
})
}
fn is_string_format(format: &DataFormat) -> bool {
match format.kind() {
ferroui_base::input::DataFormatKind::Platform => is_text_format(format.identifier()),
_ => false,
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use std::cell::RefCell;
pub(crate) struct FakeReadableDataItem {
pub(crate) formats: Vec<String>,
pub(crate) values: Vec<(String, FakeValue)>,
pub(crate) reads: RefCell<Vec<String>>,
}
#[derive(Clone)]
pub(crate) enum FakeValue {
String(&'static str),
Bytes(Vec<u8>),
Rejected(&'static str),
}
impl FakeReadableDataItem {
pub(crate) fn new(formats: &[&str], values: &[(&str, FakeValue)]) -> Rc<Self> {
Rc::new(Self {
formats: formats.iter().map(|format| format.to_string()).collect(),
values: values.iter().map(|(format, value)| (format.to_string(), value.clone())).collect(),
reads: RefCell::new(Vec::new()),
})
}
fn value(&self, format: &str) -> Result<Option<ReadableDataValueContent>, PromiseError> {
self.reads.borrow_mut().push(format.to_string());
match self.values.iter().find(|(candidate, _)| candidate == format).map(|(_, value)| value) {
Some(FakeValue::String(text)) => Ok(Some(ReadableDataValueContent::String(text.to_string()))),
Some(FakeValue::Bytes(bytes)) => Ok(Some(ReadableDataValueContent::Bytes(bytes.clone()))),
Some(FakeValue::Rejected(message)) => Err(PromiseError::new("Error", *message)),
None => Ok(None),
}
}
}
impl IReadableDataItem for FakeReadableDataItem {
fn formats(&self) -> Vec<String> {
self.formats.clone()
}
fn try_get_value_async(
&self,
format: &str,
) -> LocalBoxFuture<Result<Option<ReadableDataValueContent>, PromiseError>> {
Box::pin(std::future::ready(self.value(format)))
}
fn try_get_value(&self, format: &str) -> Option<ReadableDataValueContent> {
self.value(format).ok().flatten()
}
}
pub(crate) struct FakeReadableDataItems(pub(crate) Vec<Rc<FakeReadableDataItem>>);
impl IReadableDataItems for FakeReadableDataItems {
fn len(&self) -> usize {
self.0.len()
}
fn get(&self, index: usize) -> Rc<dyn IReadableDataItem> {
self.0[index].clone()
}
}
fn string_of(value: Option<Rc<dyn Any>>) -> Option<String> {
value.and_then(|value| value.downcast_ref::<String>().cloned())
}
fn bytes_of(value: Option<Rc<dyn Any>>) -> Option<Vec<u8>> {
value.and_then(|value| value.downcast_ref::<Rc<[u8]>>().map(|bytes| bytes.to_vec()))
}
#[test]
fn the_formats_of_an_item_are_its_format_strings() {
let item = FakeReadableDataItem::new(&["text/plain", "text/html", "application/frn-fmt.custom"], &[]);
let formats = get_readable_item_formats(&*item);
assert_eq!(3, formats.len());
assert!(DataFormat::text() == formats[0]);
assert_eq!("text/html", formats[1].identifier());
assert_eq!("custom", formats[2].identifier());
}
#[test]
fn a_png_adds_the_bitmap_format_wherever_it_is() {
let item = FakeReadableDataItem::new(&["image/png", "text/plain"], &[]);
let formats = get_readable_item_formats(&*item);
assert_eq!(3, formats.len());
assert!(DataFormat::bitmap() == formats[2]);
let item = FakeReadableDataItem::new(&["text/plain"], &[]);
assert!(!get_readable_item_formats(&*item).iter().any(|format| DataFormat::bitmap() == *format));
}
#[test]
fn no_value_is_no_value() {
assert!(try_get_value(None, &DataFormat::text().into()).unwrap().is_none());
}
#[test]
fn text_is_a_string_value() {
let text: DataFormat = DataFormat::text().into();
let value = try_get_value(Some(ReadableDataValueContent::String("hello".into())), &text).unwrap();
assert_eq!(Some("hello".to_string()), string_of(value));
let value = try_get_value(Some(ReadableDataValueContent::Bytes(b"hello".to_vec())), &text).unwrap();
assert!(value.is_none());
}
#[test]
fn text_media_types_are_strings_and_bytes_are_decoded_as_utf8() {
let html = to_data_format("text/html");
let value = try_get_value(Some(ReadableDataValueContent::String("<b>a</b>".into())), &html).unwrap();
assert_eq!(Some("<b>a</b>".to_string()), string_of(value));
let value =
try_get_value(Some(ReadableDataValueContent::Bytes("é".as_bytes().to_vec())), &html).unwrap();
assert_eq!(Some("é".to_string()), string_of(value));
let value = try_get_value(Some(ReadableDataValueContent::Bytes(vec![0x61, 0xff])), &html).unwrap();
assert_eq!(Some("a\u{fffd}".to_string()), string_of(value));
}
#[test]
fn other_formats_are_bytes_and_strings_are_encoded_as_utf8() {
let custom = to_data_format("application/frn-fmt.custom");
let value = try_get_value(Some(ReadableDataValueContent::Bytes(vec![1, 2, 3])), &custom).unwrap();
assert_eq!(Some(vec![1, 2, 3]), bytes_of(value));
let value = try_get_value(Some(ReadableDataValueContent::String("é".into())), &custom).unwrap();
assert_eq!(Some("é".as_bytes().to_vec()), bytes_of(value));
}
#[test]
fn a_bitmap_is_only_read_from_bytes() {
let bitmap: DataFormat = DataFormat::bitmap().into();
assert!(try_get_value(Some(ReadableDataValueContent::String("png".into())), &bitmap).unwrap().is_none());
}
}