// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use std::fmt::Write; use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageOcclusionProperty; use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageOcclusionShape; use htmlescape::encode_attribute; use nom::bytes::complete::escaped; use nom::bytes::complete::is_not; use nom::bytes::complete::tag; use nom::character::complete::char; use nom::error::ErrorKind; use nom::sequence::preceded; use nom::sequence::separated_pair; fn unescape(text: &str) -> String { text.replace("\\:", ":") } pub fn parse_image_cloze(text: &str) -> Option { if let Some((shape, _)) = text.split_once(':') { let mut properties = vec![]; let mut remaining = &text[shape.len()..]; while let Ok((rem, (name, value))) = separated_pair::<_, _, _, _, (_, ErrorKind), _, _, _>( preceded(tag(":"), is_not("=")), tag("="), escaped(is_not("\\:"), '\\', char(':')), )(remaining) { remaining = rem; let value = unescape(value); properties.push(ImageOcclusionProperty { name: name.to_string(), value, }) } return Some(ImageOcclusionShape { shape: shape.to_string(), properties, }); } None } // convert text like // rect:left=.2325:top=.3261:width=.202:height=.0975 // to something like // result = "data-shape="rect" data-left="399.01" data-top="99.52" // data-width="167.09" data-height="33.78" pub fn get_image_cloze_data(text: &str) -> String { let mut result = String::new(); if let Some(occlusion) = parse_image_cloze(text) { if !occlusion.shape.is_empty() && matches!( occlusion.shape.as_str(), "rect" | "ellipse" | "polygon" | "text" ) { result.push_str(&format!("data-shape=\"{}\" ", occlusion.shape)); } for property in occlusion.properties { match property.name.as_str() { "left" => { if !property.value.is_empty() { result.push_str(&format!("data-left=\"{}\" ", property.value)); } } "top" => { if !property.value.is_empty() { result.push_str(&format!("data-top=\"{}\" ", property.value)); } } "width" => { if !is_empty_or_zero(&property.value) { result.push_str(&format!("data-width=\"{}\" ", property.value)); } } "height" => { if !is_empty_or_zero(&property.value) { result.push_str(&format!("data-height=\"{}\" ", property.value)); } } "rx" => { if !is_empty_or_zero(&property.value) { result.push_str(&format!("data-rx=\"{}\" ", property.value)); } } "ry" => { if !is_empty_or_zero(&property.value) { result.push_str(&format!("data-ry=\"{}\" ", property.value)); } } "points" => { if !property.value.is_empty() { let mut point_str = String::new(); for point_pair in property.value.split(' ') { let Some((x, y)) = point_pair.split_once(',') else { continue; }; write!(&mut point_str, "{},{} ", x, y).unwrap(); } // remove the trailing space point_str.pop(); if !point_str.is_empty() { result.push_str(&format!("data-points=\"{point_str}\" ")); } } } "oi" => { if !property.value.is_empty() { result.push_str(&format!("data-occludeInactive=\"{}\" ", property.value)); } } "text" => { if !property.value.is_empty() { result.push_str(&format!( "data-text=\"{}\" ", encode_attribute(&property.value) )); } } "scale" => { if !is_empty_or_zero(&property.value) { result.push_str(&format!("data-scale=\"{}\" ", property.value)); } } "fs" => { if !property.value.is_empty() { result.push_str(&format!("data-font-size=\"{}\" ", property.value)); } } _ => {} } } } result } fn is_empty_or_zero(text: &str) -> bool { text.is_empty() || text == "0" } //---------------------------------------- // Tests //---------------------------------------- #[test] fn test_get_image_cloze_data() { assert_eq!( get_image_cloze_data("rect:left=10:top=20:width=30:height=10"), format!( r#"data-shape="rect" data-left="10" data-top="20" data-width="30" data-height="10" "#, ) ); assert_eq!( get_image_cloze_data("ellipse:left=15:top=20:width=10:height=20:rx=10:ry=5"), r#"data-shape="ellipse" data-left="15" data-top="20" data-width="10" data-height="20" data-rx="10" data-ry="5" "#, ); assert_eq!( get_image_cloze_data("polygon:points=0,0 10,10 20,0"), r#"data-shape="polygon" data-points="0,0 10,10 20,0" "#, ); assert_eq!( get_image_cloze_data("text:text=foo\\:bar:left=10"), r#"data-shape="text" data-text="foo:bar" data-left="10" "#, ); }