├── .gitignore ├── Cargo.toml └── src ├── de.rs ├── lib.rs ├── markdown ├── attrs.rs ├── content.rs ├── from_markdown.rs ├── helper.rs ├── mod.rs ├── schema.rs └── to_markdown.rs ├── model ├── content.rs ├── fragment.rs ├── marks.rs ├── mod.rs ├── node.rs ├── replace.rs ├── resolved_pos.rs ├── schema.rs └── util.rs ├── transform ├── mark_step.rs ├── mod.rs ├── replace_step.rs ├── step.rs └── util.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prosemirror" 3 | version = "0.1.0" 4 | authors = ["Xiphoseer"] 5 | edition = "2018" 6 | 7 | [features] 8 | cmark = ["pulldown-cmark", "pulldown-cmark-to-cmark"] 9 | 10 | [dev-dependencies] 11 | serde_json = "1.0.53" 12 | 13 | [dependencies] 14 | derivative = "2.2.0" 15 | thiserror = "1.0" 16 | displaydoc = "0.1" 17 | derive-new = "0.5" 18 | pulldown-cmark = { version = "0.7", optional = true } 19 | pulldown-cmark-to-cmark = { version = "4.0", optional = true } 20 | 21 | [dependencies.serde] 22 | version = "1.0.97" 23 | features = ["derive"] 24 | -------------------------------------------------------------------------------- /src/de.rs: -------------------------------------------------------------------------------- 1 | use serde::de::{Deserialize, Deserializer}; 2 | 3 | pub fn deserialize_or_default<'de, T, D>(deserializer: D) -> Result 4 | where 5 | D: Deserializer<'de>, 6 | T: Deserialize<'de> + Default, 7 | { 8 | let opt_string: Option = Deserialize::deserialize(deserializer)?; 9 | Ok(opt_string.unwrap_or_default()) 10 | } 11 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | //! # The ProseMirror API 3 | //! 4 | //! This crate is a re-implementation of the [ProseMirror](https://prosemirror.net) API in Rust. 5 | //! It can be used to create a collaborative editing authority that is able to apply steps to 6 | //! a document. 7 | 8 | #[macro_use] 9 | extern crate derive_new; 10 | 11 | pub(crate) mod de; 12 | pub mod markdown; 13 | pub mod model; 14 | pub mod transform; 15 | pub mod util; 16 | -------------------------------------------------------------------------------- /src/markdown/attrs.rs: -------------------------------------------------------------------------------- 1 | use crate::de; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Attributes for a heading (i.e. `

`, `

`, ...) 5 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] 6 | pub struct HeadingAttrs { 7 | /// The level of the heading (i.e. `1` for `

`) 8 | pub level: u8, 9 | } 10 | 11 | /// Attributes for a code block 12 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] 13 | pub struct CodeBlockAttrs { 14 | /// ??? 15 | pub params: String, 16 | } 17 | 18 | /// Attributes for a bullet list 19 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] 20 | pub struct BulletListAttrs { 21 | /// ??? 22 | pub tight: bool, 23 | } 24 | 25 | /// Attributes for an ordered list 26 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] 27 | pub struct OrderedListAttrs { 28 | /// Initial value 29 | pub order: usize, 30 | /// ??? 31 | pub tight: bool, 32 | } 33 | 34 | /// Attributes for an image 35 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] 36 | pub struct ImageAttrs { 37 | /// Source URL 38 | pub src: String, 39 | #[serde(default, deserialize_with = "de::deserialize_or_default")] 40 | /// Alternative Text (Accessibility) 41 | pub alt: String, 42 | /// Title (Tooltip) 43 | #[serde(default, deserialize_with = "de::deserialize_or_default")] 44 | pub title: String, 45 | } 46 | 47 | /// The attributes for a hyperlink 48 | #[derive(Debug, Hash, Eq, Clone, PartialEq, Deserialize, Serialize)] 49 | pub struct LinkAttrs { 50 | /// The URL the link points to 51 | pub href: String, 52 | /// The title of the link 53 | #[serde(default, deserialize_with = "de::deserialize_or_default")] 54 | pub title: String, 55 | } 56 | -------------------------------------------------------------------------------- /src/markdown/content.rs: -------------------------------------------------------------------------------- 1 | use crate::markdown::{MarkdownNodeType, MD}; 2 | use crate::model::{util, ContentMatch, Fragment, Node, NodeType}; 3 | use crate::util::then_some; 4 | use std::ops::RangeBounds; 5 | 6 | /// The content match type for markdown 7 | #[derive(Copy, Clone, Eq, PartialEq)] 8 | pub enum MarkdownContentMatch { 9 | /// `inline*` 10 | InlineStar, 11 | /// `block+` 12 | BlockPlus, 13 | /// `block*` 14 | BlockStar, 15 | /// `(text | image)*` 16 | OrTextImageStar, 17 | /// `text*` 18 | TextStar, 19 | /// `list_item+` 20 | ListItemPlus, 21 | /// `list_item*` 22 | ListItemStar, 23 | /// `paragraph block*` 24 | ParagraphBlockStar, 25 | /// empty 26 | Empty, 27 | } 28 | 29 | impl ContentMatch for MarkdownContentMatch { 30 | fn match_type(self, r#type: MarkdownNodeType) -> Option { 31 | match self { 32 | Self::InlineStar => then_some(r#type.is_inline(), Self::InlineStar), 33 | Self::BlockPlus | Self::BlockStar => then_some(r#type.is_block(), Self::BlockStar), 34 | Self::OrTextImageStar => then_some( 35 | matches!(r#type, MarkdownNodeType::Text | MarkdownNodeType::Image), 36 | Self::OrTextImageStar, 37 | ), 38 | Self::TextStar => then_some(matches!(r#type, MarkdownNodeType::Text), Self::TextStar), 39 | Self::ListItemPlus | Self::ListItemStar => then_some( 40 | matches!(r#type, MarkdownNodeType::ListItem), 41 | Self::ListItemStar, 42 | ), 43 | Self::ParagraphBlockStar => then_some( 44 | matches!(r#type, MarkdownNodeType::Paragraph), 45 | Self::BlockStar, 46 | ), 47 | Self::Empty => None, 48 | } 49 | } 50 | 51 | fn match_fragment_range>( 52 | self, 53 | fragment: &Fragment, 54 | range: R, 55 | ) -> Option { 56 | let start = util::from(&range); 57 | let end = util::to(&range, fragment.child_count()); 58 | 59 | let mut test = self; 60 | for child in &fragment.children()[start..end] { 61 | match test.match_type(child.r#type()) { 62 | Some(next) => { 63 | test = next; 64 | } 65 | None => { 66 | return None; 67 | } 68 | } 69 | } 70 | Some(test) 71 | } 72 | 73 | fn valid_end(self) -> bool { 74 | matches!( 75 | self, 76 | Self::InlineStar 77 | | Self::BlockStar 78 | | Self::OrTextImageStar 79 | | Self::TextStar 80 | | Self::ListItemStar 81 | | Self::Empty 82 | ) 83 | } 84 | } 85 | 86 | impl MarkdownContentMatch { 87 | pub(crate) fn compatible(self, other: Self) -> bool { 88 | match self { 89 | Self::InlineStar => matches!( 90 | other, 91 | Self::InlineStar | Self::OrTextImageStar | Self::TextStar 92 | ), 93 | Self::BlockPlus | Self::BlockStar => matches!( 94 | other, 95 | Self::BlockPlus | Self::ParagraphBlockStar | Self::BlockStar 96 | ), 97 | Self::OrTextImageStar => matches!( 98 | other, 99 | Self::InlineStar | Self::OrTextImageStar | Self::TextStar 100 | ), 101 | Self::TextStar => matches!( 102 | other, 103 | Self::InlineStar | Self::OrTextImageStar | Self::TextStar 104 | ), 105 | Self::ListItemPlus | Self::ListItemStar => { 106 | matches!(other, Self::ListItemPlus | Self::ListItemStar) 107 | } 108 | Self::ParagraphBlockStar => matches!(other, Self::BlockPlus | Self::ParagraphBlockStar), 109 | Self::Empty => false, 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/markdown/from_markdown.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | BulletListAttrs, CodeBlockAttrs, HeadingAttrs, ImageAttrs, LinkAttrs, MarkdownMark, 3 | MarkdownNode, OrderedListAttrs, MD, 4 | }; 5 | use crate::model::{AttrNode, Block, Fragment, Leaf, MarkSet, Text, TextNode}; 6 | use displaydoc::Display; 7 | use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag}; 8 | use std::{ 9 | convert::{TryFrom, TryInto}, 10 | num::TryFromIntError, 11 | }; 12 | use thiserror::Error; 13 | 14 | /// Errors that can occur when reading a markdown file 15 | #[derive(Debug, PartialEq, Display, Error)] 16 | pub enum FromMarkdownError { 17 | /// Heading level too deep 18 | LevelMismatch(#[from] TryFromIntError), 19 | /// Not supported: `{0}` 20 | NotSupported(&'static str), 21 | /// The stack was empty 22 | StackEmpty, 23 | /// Event mismatch 24 | MisplacedEndTag(&'static str, Attrs), 25 | /// No children allowed in {0:?} 26 | NoChildrenAllowed(&'static str), 27 | } 28 | 29 | #[derive(Debug, Clone, PartialEq, Eq)] 30 | pub enum Attrs { 31 | Doc, 32 | Paragraph, 33 | Heading(HeadingAttrs), 34 | Blockquote, 35 | CodeBlock(CodeBlockAttrs), 36 | OrderedList(OrderedListAttrs), 37 | BulletList(BulletListAttrs), 38 | ListItem, 39 | Image(ImageAttrs), 40 | } 41 | 42 | /// Creates a MarkdownNode::Doc from a text 43 | pub fn from_markdown(text: &str) -> Result { 44 | let parser = Parser::new(text); 45 | let mut d = MarkdownDeserializer::default(); 46 | d.deserialize(parser) 47 | } 48 | 49 | #[derive(Default)] 50 | pub struct MarkdownDeserializer { 51 | stack: Vec<(Vec, Attrs)>, 52 | mark_set: MarkSet, 53 | } 54 | 55 | impl MarkdownDeserializer { 56 | /*#[must_use] 57 | fn push_text(&mut self) -> Result<(), FromMarkdownError> { 58 | let last = self.stack.last_mut().ok_or(FromMarkdownError::StackEmpty)?; 59 | if !self.text.is_empty() { 60 | last.0.push(MarkdownNode::Text(TextNode { 61 | marks: self.mark_set.clone(), 62 | text: Text::from(std::mem::take(&mut self.text)), 63 | })); 64 | } 65 | Ok(()) 66 | }*/ 67 | 68 | fn push_stack(&mut self, attrs: Attrs) { 69 | self.stack.push((Vec::new(), attrs)); 70 | } 71 | 72 | fn pop_stack(&mut self) -> Result<(Vec, Attrs), FromMarkdownError> { 73 | let popped = self.stack.pop().ok_or(FromMarkdownError::StackEmpty)?; 74 | Ok(popped) 75 | } 76 | 77 | fn add_content(&mut self, node: MarkdownNode) -> Result<(), FromMarkdownError> { 78 | let last = self.stack.last_mut().ok_or(FromMarkdownError::StackEmpty)?; 79 | last.0.push(node); 80 | Ok(()) 81 | } 82 | 83 | fn deserialize(&mut self, parser: Parser) -> Result { 84 | self.push_stack(Attrs::Doc); 85 | for event in parser { 86 | match event { 87 | Event::Start(tag) => match tag { 88 | Tag::Paragraph => { 89 | self.stack.push((Vec::new(), Attrs::Paragraph)); 90 | } 91 | Tag::Heading(l) => { 92 | let level = u8::try_from(l)?; 93 | self.stack 94 | .push((Vec::new(), Attrs::Heading(HeadingAttrs { level }))); 95 | } 96 | Tag::BlockQuote => { 97 | self.stack.push((Vec::new(), Attrs::Blockquote)); 98 | } 99 | Tag::CodeBlock(kind) => { 100 | let params = if let CodeBlockKind::Fenced(params) = kind { 101 | params.to_string() 102 | } else { 103 | String::new() 104 | }; 105 | self.stack 106 | .push((Vec::new(), Attrs::CodeBlock(CodeBlockAttrs { params }))); 107 | } 108 | Tag::List(ord) => { 109 | if let Some(order) = ord { 110 | self.stack.push(( 111 | Vec::new(), 112 | Attrs::OrderedList(OrderedListAttrs { 113 | order: order.try_into()?, // TODO: other error 114 | tight: false, 115 | }), 116 | )) 117 | } else { 118 | self.stack.push(( 119 | Vec::new(), 120 | Attrs::BulletList(BulletListAttrs { tight: false }), 121 | )); 122 | } 123 | } 124 | Tag::Item => { 125 | self.stack.push((Vec::new(), Attrs::ListItem)); 126 | } 127 | Tag::FootnoteDefinition(_) => { 128 | return Err(FromMarkdownError::NotSupported("FootnoteDefinition")); 129 | } 130 | Tag::Table(_) => { 131 | return Err(FromMarkdownError::NotSupported("Table")); 132 | } 133 | Tag::TableHead => { 134 | return Err(FromMarkdownError::NotSupported("TableHead")); 135 | } 136 | Tag::TableRow => { 137 | return Err(FromMarkdownError::NotSupported("TableRow")); 138 | } 139 | Tag::TableCell => { 140 | return Err(FromMarkdownError::NotSupported("TableCell")); 141 | } 142 | Tag::Emphasis => { 143 | self.mark_set.add(&MarkdownMark::Em); 144 | } 145 | Tag::Strong => { 146 | self.mark_set.add(&MarkdownMark::Strong); 147 | } 148 | Tag::Strikethrough => { 149 | return Err(FromMarkdownError::NotSupported("Strikethrough")); 150 | } 151 | Tag::Link(_, href, title) => { 152 | self.mark_set.add(&MarkdownMark::Link { 153 | attrs: LinkAttrs { 154 | href: href.to_string(), 155 | title: title.to_string(), 156 | }, 157 | }); 158 | } 159 | Tag::Image(_, src, title) => { 160 | self.push_stack(Attrs::Image(ImageAttrs { 161 | src: src.to_string(), 162 | alt: title.to_string(), 163 | title: title.to_string(), 164 | })); 165 | } 166 | }, 167 | Event::End(tag) => match tag { 168 | Tag::Paragraph => { 169 | let (content, attrs) = self.pop_stack()?; 170 | if matches!(attrs, Attrs::Paragraph) { 171 | let p = MarkdownNode::Paragraph(Block { 172 | content: Fragment::from(content), 173 | }); 174 | self.add_content(p)?; 175 | } else { 176 | return Err(FromMarkdownError::MisplacedEndTag("Paragraph", attrs)); 177 | } 178 | } 179 | Tag::Heading(_) => { 180 | let (content, attrs) = self.pop_stack()?; 181 | if let Attrs::Heading(attrs) = attrs { 182 | let h = MarkdownNode::Heading(AttrNode { 183 | attrs, 184 | content: Fragment::from(content), 185 | }); 186 | self.add_content(h)?; 187 | } else { 188 | return Err(FromMarkdownError::MisplacedEndTag("Heading", attrs)); 189 | } 190 | } 191 | Tag::BlockQuote => { 192 | let (content, attrs) = self.pop_stack()?; 193 | if let Attrs::Blockquote = attrs { 194 | let b = MarkdownNode::Blockquote(Block { 195 | content: Fragment::from(content), 196 | }); 197 | self.add_content(b)?; 198 | } else { 199 | return Err(FromMarkdownError::MisplacedEndTag("BlockQuote", attrs)); 200 | } 201 | } 202 | Tag::CodeBlock(_) => { 203 | let (mut content, attrs) = self.pop_stack()?; 204 | if let Attrs::CodeBlock(attrs) = attrs { 205 | if let Some(MarkdownNode::Text(t)) = content.last_mut() { 206 | t.text.remove_last_newline(); 207 | } 208 | let cb = MarkdownNode::CodeBlock(AttrNode { 209 | attrs, 210 | content: Fragment::from(content), 211 | }); 212 | self.add_content(cb)?; 213 | } else { 214 | return Err(FromMarkdownError::MisplacedEndTag("CodeBlock", attrs)); 215 | } 216 | } 217 | Tag::List(_) => { 218 | let (content, attrs) = self.pop_stack()?; 219 | match attrs { 220 | Attrs::BulletList(attrs) => { 221 | let l = MarkdownNode::BulletList(AttrNode { 222 | attrs, 223 | content: Fragment::from(content), 224 | }); 225 | self.add_content(l)?; 226 | } 227 | Attrs::OrderedList(attrs) => { 228 | let l = MarkdownNode::OrderedList(AttrNode { 229 | attrs, 230 | content: Fragment::from(content), 231 | }); 232 | self.add_content(l)?; 233 | } 234 | _ => { 235 | return Err(FromMarkdownError::MisplacedEndTag("List", attrs)); 236 | } 237 | } 238 | } 239 | Tag::Item => { 240 | let (content, attrs) = self.pop_stack()?; 241 | if let Attrs::ListItem = attrs { 242 | let cb = MarkdownNode::ListItem(Block { 243 | content: Fragment::from(content), 244 | }); 245 | self.add_content(cb)?; 246 | } 247 | } 248 | Tag::FootnoteDefinition(_) => { 249 | return Err(FromMarkdownError::NotSupported("FootnoteDefinition")); 250 | } 251 | Tag::Table(_) => { 252 | return Err(FromMarkdownError::NotSupported("Table")); 253 | } 254 | Tag::TableHead => { 255 | return Err(FromMarkdownError::NotSupported("TableHead")); 256 | } 257 | Tag::TableRow => { 258 | return Err(FromMarkdownError::NotSupported("TableRow")); 259 | } 260 | Tag::TableCell => { 261 | return Err(FromMarkdownError::NotSupported("TableCell")); 262 | } 263 | Tag::Emphasis => { 264 | self.mark_set.remove(&MarkdownMark::Em); 265 | } 266 | Tag::Strong => { 267 | self.mark_set.remove(&MarkdownMark::Strong); 268 | } 269 | Tag::Strikethrough => { 270 | return Err(FromMarkdownError::NotSupported("Strikethrough")); 271 | } 272 | Tag::Link(_, href, title) => self.mark_set.remove(&MarkdownMark::Link { 273 | attrs: LinkAttrs { 274 | href: href.to_string(), 275 | title: title.to_string(), 276 | }, 277 | }), 278 | Tag::Image(_, _, _) => { 279 | let (content, attrs) = self.pop_stack()?; 280 | if let Attrs::Image(attrs) = attrs { 281 | if content.len() > 0 { 282 | return Err(FromMarkdownError::NoChildrenAllowed("Image")); 283 | } 284 | let cb = MarkdownNode::Image(Leaf { attrs }); 285 | self.add_content(cb)?; 286 | } else { 287 | return Err(FromMarkdownError::MisplacedEndTag("Image", attrs)); 288 | } 289 | } 290 | }, 291 | Event::Text(text) => { 292 | self.add_content(MarkdownNode::Text(TextNode { 293 | text: Text::from(text.to_string()), 294 | marks: self.mark_set.clone(), 295 | }))?; 296 | } 297 | Event::Code(text) => { 298 | let mut marks = self.mark_set.clone(); 299 | marks.add(&MarkdownMark::Code); 300 | self.add_content(MarkdownNode::Text(TextNode { 301 | text: Text::from(text.to_string()), 302 | marks, 303 | }))?; 304 | } 305 | Event::Html(_) => { 306 | return Err(FromMarkdownError::NotSupported("Html")); 307 | } 308 | Event::FootnoteReference(_) => { 309 | return Err(FromMarkdownError::NotSupported("FootnoteReference")); 310 | } 311 | Event::SoftBreak => { 312 | return Err(FromMarkdownError::NotSupported("SoftBreak")); 313 | } 314 | Event::HardBreak => { 315 | self.add_content(MarkdownNode::HardBreak)?; 316 | } 317 | Event::Rule => { 318 | self.add_content(MarkdownNode::HorizontalRule)?; 319 | } 320 | Event::TaskListMarker(_) => { 321 | return Err(FromMarkdownError::NotSupported("TaskListMarker")); 322 | } 323 | } 324 | } 325 | let (content, attrs) = self.pop_stack()?; 326 | if let Attrs::Doc = attrs { 327 | Ok(MarkdownNode::Doc(Block { 328 | content: Fragment::from(content), 329 | })) 330 | } else { 331 | Err(FromMarkdownError::MisplacedEndTag("Doc", attrs)) 332 | } 333 | } 334 | } 335 | 336 | #[cfg(test)] 337 | mod tests { 338 | use pulldown_cmark::{CowStr, Event, Parser, Tag}; 339 | 340 | #[test] 341 | fn test_alerts() { 342 | let test_string = "\ 343 | ### Alert Area\n\ 344 | \n\ 345 | :::success\n\ 346 | Yes :tada:\n\ 347 | :::\n\ 348 | "; 349 | 350 | let p = Parser::new(test_string); 351 | let v: Vec = p.collect(); 352 | assert_eq!( 353 | v, 354 | vec![ 355 | Event::Start(Tag::Heading(3)), 356 | Event::Text(CowStr::Borrowed("Alert Area")), 357 | Event::End(Tag::Heading(3)), 358 | Event::Start(Tag::Paragraph), 359 | Event::Text(CowStr::Borrowed(":::success")), 360 | Event::SoftBreak, 361 | Event::Text(CowStr::Borrowed("Yes :tada:")), 362 | Event::SoftBreak, 363 | Event::Text(CowStr::Borrowed(":::")), 364 | Event::End(Tag::Paragraph), 365 | ] 366 | ); 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/markdown/helper.rs: -------------------------------------------------------------------------------- 1 | //! # Helpers 2 | //! 3 | //! This module contains some functions to create nodes programmatically. 4 | //! 5 | //! See also: 6 | use super::{BulletListAttrs, CodeBlockAttrs, HeadingAttrs, MarkdownMark, MarkdownNode, MD}; 7 | use crate::model::{self, AttrNode, Block, Mark, Text, TextNode}; 8 | 9 | type Fragment = model::Fragment; 10 | 11 | impl From<&str> for Fragment { 12 | fn from(s: &str) -> Fragment { 13 | Fragment::from(vec![MarkdownNode::from(s)]) 14 | } 15 | } 16 | 17 | impl From for Fragment { 18 | fn from(node: MarkdownNode) -> Fragment { 19 | Fragment::from(vec![node]) 20 | } 21 | } 22 | 23 | /// Create a document node. 24 | pub fn doc>(content: A) -> MarkdownNode { 25 | MarkdownNode::Doc(Block { 26 | content: content.into(), 27 | }) 28 | } 29 | 30 | /// Create a heading node. 31 | pub fn h>(level: u8, content: A) -> MarkdownNode { 32 | MarkdownNode::Heading(AttrNode { 33 | attrs: HeadingAttrs { level }, 34 | content: content.into(), 35 | }) 36 | } 37 | 38 | /// Create a heading (level 1) node. 39 | pub fn h1>(content: A) -> MarkdownNode { 40 | h(1, content) 41 | } 42 | 43 | /// Create a heading (level 2) node. 44 | pub fn h2>(content: A) -> MarkdownNode { 45 | h(2, content) 46 | } 47 | 48 | /// Create an emphasized text node. 49 | pub fn em(content: &str) -> MarkdownNode { 50 | MarkdownNode::Text(TextNode { 51 | text: Text::from(content.to_string()), 52 | marks: MarkdownMark::Em.into_set(), 53 | }) 54 | } 55 | 56 | /// Create an emphasized text node. 57 | pub fn strong(content: &str) -> MarkdownNode { 58 | MarkdownNode::Text(TextNode { 59 | text: Text::from(content.to_string()), 60 | marks: MarkdownMark::Strong.into_set(), 61 | }) 62 | } 63 | 64 | /// Create a paragraph node. 65 | pub fn p>(content: A) -> MarkdownNode { 66 | MarkdownNode::Paragraph(Block { 67 | content: content.into(), 68 | }) 69 | } 70 | 71 | /// Create a list item node. 72 | pub fn li>(content: A) -> MarkdownNode { 73 | MarkdownNode::ListItem(Block { 74 | content: content.into(), 75 | }) 76 | } 77 | 78 | /// Create a Bullet list node. 79 | pub fn ul>(content: A) -> MarkdownNode { 80 | MarkdownNode::BulletList(AttrNode { 81 | attrs: BulletListAttrs { tight: false }, 82 | content: content.into(), 83 | }) 84 | } 85 | 86 | /// Create a code block node. 87 | pub fn code_block>(params: &str, content: A) -> MarkdownNode { 88 | MarkdownNode::CodeBlock(AttrNode { 89 | attrs: CodeBlockAttrs { 90 | params: params.to_owned(), 91 | }, 92 | content: content.into(), 93 | }) 94 | } 95 | 96 | /// Create a blockquote node. 97 | pub fn blockquote>(content: A) -> MarkdownNode { 98 | MarkdownNode::Blockquote(Block { 99 | content: content.into(), 100 | }) 101 | } 102 | 103 | /// Create a node. 104 | pub fn node>(src: A) -> MarkdownNode { 105 | src.into() 106 | } 107 | -------------------------------------------------------------------------------- /src/markdown/mod.rs: -------------------------------------------------------------------------------- 1 | //! # The markdown schema 2 | //! 3 | //! This module is derived from the `prosemirror-markdown` schema and the 4 | //! the general JSON serialization of nodes. 5 | mod attrs; 6 | mod content; 7 | pub mod helper; 8 | mod schema; 9 | 10 | #[cfg(feature = "cmark")] 11 | mod from_markdown; 12 | #[cfg(feature = "cmark")] 13 | mod to_markdown; 14 | 15 | use crate::model::{ 16 | AttrNode, Block, Fragment, Leaf, Mark, MarkSet, MarkType, Node, Text, TextNode, 17 | }; 18 | pub use attrs::{ 19 | BulletListAttrs, CodeBlockAttrs, HeadingAttrs, ImageAttrs, LinkAttrs, OrderedListAttrs, 20 | }; 21 | pub use content::MarkdownContentMatch; 22 | pub use schema::{MarkdownNodeType, MD}; 23 | 24 | #[cfg(feature = "cmark")] 25 | pub use from_markdown::{from_markdown, FromMarkdownError}; 26 | #[cfg(feature = "cmark")] 27 | pub use to_markdown::{to_markdown, ToMarkdownError}; 28 | 29 | use derivative::Derivative; 30 | use serde::{Deserialize, Serialize}; 31 | 32 | /// The node type for the markdown schema 33 | #[derive(Debug, Derivative, Deserialize, Serialize, PartialEq, Eq)] 34 | #[derivative(Clone(bound = ""))] 35 | #[serde(tag = "type", rename_all = "snake_case")] 36 | pub enum MarkdownNode { 37 | /// The document root 38 | Doc(Block), 39 | /// A heading, e.g. `

` 40 | Heading(AttrNode), 41 | /// A code block 42 | CodeBlock(AttrNode), 43 | /// A text node 44 | Text(TextNode), 45 | /// A blockquote 46 | Blockquote(Block), 47 | /// A paragraph 48 | Paragraph(Block), 49 | /// A bullet list 50 | BulletList(AttrNode), 51 | /// An ordered list 52 | OrderedList(AttrNode), 53 | /// A list item 54 | ListItem(Block), 55 | /// A horizontal line `
` 56 | HorizontalRule, 57 | /// A hard break `
` 58 | HardBreak, 59 | /// An image `` 60 | Image(Leaf), 61 | } 62 | 63 | impl From> for MarkdownNode { 64 | fn from(text_node: TextNode) -> Self { 65 | Self::Text(text_node) 66 | } 67 | } 68 | 69 | impl Node for MarkdownNode { 70 | fn text_node(&self) -> Option<&TextNode> { 71 | if let Self::Text(node) = self { 72 | Some(node) 73 | } else { 74 | None 75 | } 76 | } 77 | 78 | fn new_text_node(node: TextNode) -> Self { 79 | Self::Text(node) 80 | } 81 | 82 | fn is_block(&self) -> bool { 83 | match self { 84 | Self::Doc { .. } => true, 85 | Self::Paragraph { .. } => true, 86 | Self::Blockquote { .. } => true, 87 | Self::HorizontalRule => true, 88 | Self::Heading { .. } => true, 89 | Self::CodeBlock { .. } => true, 90 | Self::OrderedList { .. } => true, 91 | Self::BulletList { .. } => true, 92 | Self::ListItem { .. } => true, 93 | Self::Text { .. } => false, 94 | Self::Image { .. } => false, 95 | Self::HardBreak => false, 96 | } 97 | } 98 | 99 | fn r#type(&self) -> MarkdownNodeType { 100 | match self { 101 | Self::Doc { .. } => MarkdownNodeType::Doc, 102 | Self::Paragraph { .. } => MarkdownNodeType::Paragraph, 103 | Self::Blockquote { .. } => MarkdownNodeType::Blockquote, 104 | Self::HorizontalRule => MarkdownNodeType::HorizontalRule, 105 | Self::Heading { .. } => MarkdownNodeType::Heading, 106 | Self::CodeBlock { .. } => MarkdownNodeType::CodeBlock, 107 | Self::OrderedList { .. } => MarkdownNodeType::OrderedList, 108 | Self::BulletList { .. } => MarkdownNodeType::BulletList, 109 | Self::ListItem { .. } => MarkdownNodeType::ListItem, 110 | Self::Text { .. } => MarkdownNodeType::Text, 111 | Self::Image { .. } => MarkdownNodeType::Image, 112 | Self::HardBreak => MarkdownNodeType::HardBreak, 113 | } 114 | } 115 | 116 | fn text>(text: A) -> Self { 117 | Self::Text(TextNode { 118 | text: Text::from(text.into()), 119 | marks: MarkSet::::default(), 120 | }) 121 | } 122 | 123 | fn content(&self) -> Option<&Fragment> { 124 | match self { 125 | Self::Doc(doc) => Some(&doc.content), 126 | Self::Heading(AttrNode { content, .. }) => Some(content), 127 | Self::CodeBlock(AttrNode { content, .. }) => Some(content), 128 | Self::Text { .. } => None, 129 | Self::Blockquote(Block { content }) => Some(content), 130 | Self::Paragraph(Block { content }) => Some(content), 131 | Self::BulletList(AttrNode { content, .. }) => Some(content), 132 | Self::OrderedList(AttrNode { content, .. }) => Some(content), 133 | Self::ListItem(Block { content }) => Some(content), 134 | Self::HorizontalRule => None, 135 | Self::HardBreak => None, 136 | Self::Image { .. } => None, 137 | } 138 | } 139 | 140 | fn marks(&self) -> Option<&MarkSet> { 141 | None 142 | } 143 | 144 | fn mark(&self, set: MarkSet) -> Self { 145 | // TODO: marks on other nodes 146 | if let Some(text_node) = self.text_node() { 147 | Self::Text(TextNode { 148 | marks: set, 149 | text: text_node.text.clone(), 150 | }) 151 | } else { 152 | self.clone() 153 | } 154 | } 155 | 156 | fn copy(&self, map: F) -> Self 157 | where 158 | F: FnOnce(&Fragment) -> Fragment, 159 | { 160 | match self { 161 | Self::Doc(block) => Self::Doc(block.copy(map)), 162 | Self::Heading(node) => Self::Heading(node.copy(map)), 163 | Self::CodeBlock(node) => Self::CodeBlock(node.copy(map)), 164 | Self::Text(node) => Self::Text(node.clone()), 165 | Self::Blockquote(block) => Self::Blockquote(block.copy(map)), 166 | Self::Paragraph(block) => Self::Paragraph(block.copy(map)), 167 | Self::BulletList(node) => Self::BulletList(node.copy(map)), 168 | Self::OrderedList(node) => Self::OrderedList(node.copy(map)), 169 | Self::ListItem(block) => Self::ListItem(block.copy(map)), 170 | Self::HorizontalRule => Self::HorizontalRule, 171 | Self::HardBreak => Self::HardBreak, 172 | Self::Image(img) => Self::Image(img.clone()), 173 | } 174 | } 175 | } 176 | 177 | impl From<&str> for MarkdownNode { 178 | fn from(text: &str) -> Self { 179 | Self::text(text) 180 | } 181 | } 182 | 183 | /// The marks that can be on some span 184 | #[derive(Debug, Hash, Eq, Clone, PartialEq, Deserialize, Serialize)] 185 | #[serde(tag = "type", rename_all = "camelCase")] 186 | pub enum MarkdownMark { 187 | /// bold 188 | Strong, 189 | /// italics 190 | Em, 191 | /// monospace 192 | Code, 193 | /// hyper-linked 194 | Link { 195 | /// The attributes 196 | attrs: LinkAttrs, 197 | }, 198 | } 199 | 200 | impl Mark for MarkdownMark { 201 | fn r#type(&self) -> MarkdownMarkType { 202 | match self { 203 | Self::Strong => MarkdownMarkType::Strong, 204 | Self::Em => MarkdownMarkType::Em, 205 | Self::Code => MarkdownMarkType::Code, 206 | Self::Link { .. } => MarkdownMarkType::Link, 207 | } 208 | } 209 | } 210 | 211 | /// The type of a markdown mark. 212 | #[derive(Debug, Hash, Eq, Copy, Clone, PartialEq, PartialOrd, Ord)] 213 | pub enum MarkdownMarkType { 214 | /// bold 215 | Strong, 216 | /// italics 217 | Em, 218 | /// monospace 219 | Code, 220 | /// hyper-linked 221 | Link, 222 | } 223 | 224 | impl MarkType for MarkdownMarkType {} 225 | -------------------------------------------------------------------------------- /src/markdown/schema.rs: -------------------------------------------------------------------------------- 1 | use super::MarkdownMarkType; 2 | use crate::markdown::{MarkdownContentMatch, MarkdownMark, MarkdownNode}; 3 | use crate::model::{ContentMatch, Fragment, MarkSet, Node, NodeType, Schema}; 4 | 5 | /// The markdown schema type 6 | pub struct MD; 7 | 8 | impl Schema for MD { 9 | type Node = MarkdownNode; 10 | type Mark = MarkdownMark; 11 | type MarkType = MarkdownMarkType; 12 | type NodeType = MarkdownNodeType; 13 | type ContentMatch = MarkdownContentMatch; 14 | } 15 | 16 | /// The node-spec type for the markdown schema 17 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 18 | pub enum MarkdownNodeType { 19 | /// The document root 20 | Doc, 21 | /// A heading, e.g. `

` 22 | Heading, 23 | /// A code block 24 | CodeBlock, 25 | /// A text node 26 | Text, 27 | /// A blockquote 28 | Blockquote, 29 | /// A paragraph 30 | Paragraph, 31 | /// A bullet list 32 | BulletList, 33 | /// An ordered list 34 | OrderedList, 35 | /// A list item 36 | ListItem, 37 | /// A horizontal line `
` 38 | HorizontalRule, 39 | /// A hard break `
` 40 | HardBreak, 41 | /// An image `` 42 | Image, 43 | } 44 | 45 | impl MarkdownNodeType { 46 | fn _allow_marks(self) -> bool { 47 | match self { 48 | Self::Doc 49 | | Self::Blockquote 50 | | Self::BulletList 51 | | Self::OrderedList 52 | | Self::ListItem => false, // block && !textblock 53 | 54 | Self::CodeBlock => false, // marks = "" 55 | 56 | Self::Heading | Self::Paragraph => true, // textblock 57 | 58 | Self::Text | Self::HorizontalRule | Self::HardBreak | Self::Image => true, // inline 59 | } 60 | } 61 | } 62 | 63 | impl NodeType for MarkdownNodeType { 64 | fn allow_marks(self, _marks: &MarkSet) -> bool { 65 | self._allow_marks() 66 | } 67 | 68 | fn allows_mark_type(self, _mark_type: MarkdownMarkType) -> bool { 69 | self._allow_marks() 70 | } 71 | 72 | fn is_inline(self) -> bool { 73 | matches!(self, Self::Text | Self::Image | Self::HardBreak) 74 | } 75 | 76 | fn is_block(self) -> bool { 77 | matches!( 78 | self, 79 | Self::Paragraph 80 | | Self::Blockquote 81 | | Self::Heading 82 | | Self::HorizontalRule 83 | | Self::CodeBlock 84 | | Self::OrderedList 85 | | Self::BulletList 86 | ) 87 | } 88 | 89 | fn content_match(self) -> MarkdownContentMatch { 90 | match self { 91 | Self::Doc => MarkdownContentMatch::BlockPlus, 92 | Self::Heading => MarkdownContentMatch::OrTextImageStar, 93 | Self::CodeBlock => MarkdownContentMatch::TextStar, 94 | Self::Text => MarkdownContentMatch::Empty, 95 | Self::Blockquote => MarkdownContentMatch::BlockPlus, 96 | Self::Paragraph => MarkdownContentMatch::InlineStar, 97 | Self::BulletList => MarkdownContentMatch::ListItemPlus, 98 | Self::OrderedList => MarkdownContentMatch::ListItemPlus, 99 | Self::ListItem => MarkdownContentMatch::ParagraphBlockStar, 100 | Self::HorizontalRule => MarkdownContentMatch::Empty, 101 | Self::HardBreak => MarkdownContentMatch::Empty, 102 | Self::Image => MarkdownContentMatch::Empty, 103 | } 104 | } 105 | 106 | fn compatible_content(self, other: Self) -> bool { 107 | self == other || self.content_match().compatible(other.content_match()) 108 | } 109 | 110 | /// Returns true if the given fragment is valid content for this node type with the given 111 | /// attributes. 112 | fn valid_content(self, fragment: &Fragment) -> bool { 113 | let result = self.content_match().match_fragment(fragment); 114 | 115 | if let Some(m) = result { 116 | if m.valid_end() { 117 | for child in fragment.children() { 118 | if child.marks().filter(|m| !self.allow_marks(m)).is_some() { 119 | return false; 120 | } 121 | } 122 | 123 | return true; 124 | } 125 | } 126 | 127 | false 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/markdown/to_markdown.rs: -------------------------------------------------------------------------------- 1 | use super::{MarkdownMark, MarkdownNode, MD}; 2 | use crate::model::{AttrNode, Block, Fragment, Leaf, Node}; 3 | use displaydoc::Display; 4 | use pulldown_cmark::{CodeBlockKind, CowStr, Event, InlineStr, LinkType, Tag}; 5 | use pulldown_cmark_to_cmark::cmark; 6 | use thiserror::Error; 7 | 8 | /// Possible error when generating markdown 9 | #[derive(Debug, Clone, PartialEq, Eq, Display, Error)] 10 | pub struct ToMarkdownError { 11 | /// The inner error 12 | inner: std::fmt::Error, 13 | } 14 | 15 | impl From for ToMarkdownError { 16 | fn from(e: std::fmt::Error) -> ToMarkdownError { 17 | Self { inner: e } 18 | } 19 | } 20 | 21 | /// Turn a markdown document into a string 22 | pub fn to_markdown(doc: &MarkdownNode) -> Result { 23 | let mut buf = String::with_capacity(doc.node_size() + 128); 24 | let events = MarkdownSerializer::new(doc); 25 | cmark(events, &mut buf, None)?; 26 | Ok(buf) 27 | } 28 | 29 | struct MarkdownSerializer<'a> { 30 | inner: Vec<(&'a MarkdownNode, usize)>, 31 | marks: Vec<&'a MarkdownMark>, 32 | stack: Vec>, 33 | } 34 | 35 | impl<'a> MarkdownSerializer<'a> { 36 | fn new(doc: &'a MarkdownNode) -> Self { 37 | Self { 38 | inner: vec![(doc, 0)], 39 | marks: vec![], 40 | stack: vec![], 41 | } 42 | } 43 | } 44 | 45 | fn mark_tag(mark: &MarkdownMark) -> Tag { 46 | match mark { 47 | MarkdownMark::Strong => Tag::Strong, 48 | MarkdownMark::Em => Tag::Emphasis, 49 | MarkdownMark::Code => unimplemented!("Should not be pushed on the mark stack"), 50 | MarkdownMark::Link { attrs } => Tag::Link( 51 | LinkType::Inline, 52 | CowStr::Borrowed(attrs.href.as_str()), 53 | CowStr::Borrowed(attrs.title.as_str()), 54 | ), 55 | } 56 | } 57 | 58 | impl<'a> MarkdownSerializer<'a> { 59 | fn process_content( 60 | &mut self, 61 | index: usize, 62 | content: &'a Fragment, 63 | node: &'a MarkdownNode, 64 | ) -> bool { 65 | if let Some(child) = content.maybe_child(index) { 66 | self.inner.push((node, index + 1)); 67 | self.inner.push((child, 0)); 68 | false 69 | } else { 70 | true 71 | } 72 | } 73 | 74 | fn process_attr_node( 75 | &mut self, 76 | index: usize, 77 | content: &'a Fragment, 78 | attrs: &'a A, 79 | node: &'a MarkdownNode, 80 | map: F, 81 | ) -> Option> 82 | where 83 | F: FnOnce(&'a A) -> Tag<'a>, 84 | { 85 | if index == 0 { 86 | if let Some(mark) = self.marks.pop() { 87 | self.inner.push((node, 0)); 88 | return Some(Event::End(mark_tag(mark))); 89 | } 90 | } 91 | let last = self.process_content(index, content, node); 92 | if index == 0 { 93 | if last { 94 | // close the tag next 95 | self.inner.push((node, index + 1)); 96 | } 97 | Some(Event::Start(map(attrs))) 98 | } else if last { 99 | if let Some(mark) = self.marks.pop() { 100 | self.inner.push((node, index)); 101 | return Some(Event::End(mark_tag(mark))); 102 | } 103 | let tag = map(attrs); 104 | if matches!(&tag, Tag::CodeBlock(..)) { 105 | self.stack.push(Event::End(tag)); 106 | Some(Event::Text(CowStr::Inlined(InlineStr::from('\n')))) 107 | } else { 108 | Some(Event::End(tag)) 109 | } 110 | } else { 111 | self.next() 112 | } 113 | } 114 | } 115 | 116 | impl<'a> Iterator for MarkdownSerializer<'a> { 117 | type Item = Event<'a>; 118 | fn next(&mut self) -> Option { 119 | if let Some(ev) = self.stack.pop() { 120 | return Some(ev); 121 | } 122 | 123 | if let Some((node, index)) = self.inner.pop() { 124 | match node { 125 | MarkdownNode::Doc(Block { content }) => { 126 | self.process_content(index, content, node); 127 | self.next() 128 | } 129 | MarkdownNode::Heading(AttrNode { attrs, content }) => { 130 | self.process_attr_node(index, content, attrs, node, |attrs| { 131 | Tag::Heading(attrs.level.into()) 132 | }) 133 | } 134 | MarkdownNode::CodeBlock(AttrNode { attrs, content }) => { 135 | self.process_attr_node(index, content, attrs, node, |attrs| { 136 | Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::Borrowed(&attrs.params))) 137 | }) 138 | } 139 | MarkdownNode::Text(text_node) => { 140 | if let Some(last) = self.marks.last().map(|n| *n) { 141 | if !text_node.marks.contains(last) { 142 | self.inner.push((node, index)); 143 | self.marks.pop(); 144 | return Some(Event::End(mark_tag(last))); 145 | } 146 | } 147 | let mut is_code = false; 148 | for mark in &text_node.marks { 149 | if *mark == MarkdownMark::Code { 150 | is_code = true; 151 | } else if !self.marks.contains(&mark) { 152 | self.inner.push((node, index)); 153 | self.marks.push(mark); 154 | return Some(Event::Start(mark_tag(mark))); 155 | } 156 | } 157 | if is_code { 158 | Some(Event::Code(CowStr::Borrowed(text_node.text.as_str()))) 159 | } else { 160 | Some(Event::Text(CowStr::Borrowed(text_node.text.as_str()))) 161 | } 162 | } 163 | MarkdownNode::Blockquote(Block { content }) => { 164 | self.process_attr_node(index, content, &(), node, |()| Tag::BlockQuote) 165 | } 166 | MarkdownNode::Paragraph(Block { content }) => { 167 | self.process_attr_node(index, content, &(), node, |()| Tag::Paragraph) 168 | } 169 | MarkdownNode::BulletList(AttrNode { attrs, content }) => { 170 | self.process_attr_node(index, content, attrs, node, |_| Tag::List(None)) 171 | } 172 | MarkdownNode::OrderedList(AttrNode { attrs, content }) => { 173 | self.process_attr_node(index, content, attrs, node, |_| { 174 | Tag::List(Some(attrs.order as u64)) 175 | }) 176 | } 177 | MarkdownNode::ListItem(Block { content }) => { 178 | self.process_attr_node(index, content, &(), node, |()| Tag::Item) 179 | } 180 | MarkdownNode::HorizontalRule => Some(Event::Rule), 181 | MarkdownNode::HardBreak => { 182 | // todo: inline marks 183 | Some(Event::HardBreak) 184 | } 185 | MarkdownNode::Image(Leaf { attrs }) => { 186 | self.process_attr_node(index, Fragment::EMPTY_REF, &(), node, |()| { 187 | Tag::Image( 188 | LinkType::Inline, 189 | CowStr::Borrowed(attrs.src.as_str()), 190 | CowStr::Borrowed(attrs.src.as_str()), 191 | ) 192 | }) 193 | } 194 | } 195 | } else { 196 | None 197 | } 198 | } 199 | } 200 | 201 | #[cfg(test)] 202 | mod tests { 203 | 204 | use super::to_markdown; 205 | use crate::markdown::{ 206 | helper::{blockquote, code_block, doc, h1, h2, node, p, strong}, 207 | MarkdownNode, 208 | }; 209 | 210 | fn initial_doc() -> MarkdownNode { 211 | doc(vec![ 212 | h1(( 213 | "Padington", 214 | )), 215 | code_block("", ( 216 | "fn foo(a: u32) -> u32 {\n 2 * a\n}", 217 | )), 218 | h2(( 219 | "Lorem Ipsum", 220 | )), 221 | blockquote(( 222 | p(vec![ 223 | node("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. "), 224 | strong("At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."), 225 | node(" Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."), 226 | ]), 227 | )) 228 | ]) 229 | } 230 | 231 | #[test] 232 | fn test() { 233 | let node = initial_doc(); 234 | let res = "\ 235 | # Padington\n\ 236 | \n\ 237 | ````\n\ 238 | fn foo(a: u32) -> u32 {\n 2 * a\n}\n\ 239 | ````\n\ 240 | \n\ 241 | ## Lorem Ipsum\n\ 242 | \n > \n > Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. **At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.** Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.\ 243 | ".to_string(); 244 | 245 | let save = to_markdown(&node); 246 | let line = "\n---------------------------------------\n"; 247 | assert_eq!( 248 | save.as_ref(), 249 | Ok(&res), 250 | "\ngenerated:{}{}{}\n\nexpected:{}{}{}\n", 251 | line, 252 | save.as_ref().unwrap(), 253 | line, 254 | line, 255 | &res, 256 | line, 257 | ); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/model/content.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{Fragment, Schema}; 2 | use displaydoc::Display; 3 | use std::ops::RangeBounds; 4 | use thiserror::Error; 5 | 6 | /// Error on content matching 7 | #[derive(Debug, Display, Error)] 8 | pub enum ContentMatchError { 9 | /// Called contentMatchAt on a node with invalid content 10 | InvalidContent, 11 | } 12 | 13 | /// Instances of this class represent a match state of a node type's content expression, and can be 14 | /// used to find out whether further content matches here, and whether a given position is a valid end of the node. 15 | pub trait ContentMatch: Copy { 16 | /// Try to match a fragment. Returns the resulting match when successful. 17 | fn match_fragment(self, fragment: &Fragment) -> Option { 18 | self.match_fragment_range(fragment, ..) 19 | } 20 | 21 | /// Try to match a part of a fragment. Returns the resulting match when successful. 22 | fn match_fragment_range>( 23 | self, 24 | fragment: &Fragment, 25 | range: R, 26 | ) -> Option; 27 | 28 | /// True when this match state represents a valid end of the node. 29 | fn valid_end(self) -> bool; 30 | 31 | /// Match a node type, returning a match after that node if successful. 32 | fn match_type(self, r#type: S::NodeType) -> Option; 33 | } 34 | -------------------------------------------------------------------------------- /src/model/fragment.rs: -------------------------------------------------------------------------------- 1 | use super::{util, Index, Node, Schema, Text}; 2 | use derivative::Derivative; 3 | use displaydoc::Display; 4 | use serde::{Deserialize, Serialize, Serializer}; 5 | use std::borrow::Cow; 6 | use std::ops::RangeBounds; 7 | use thiserror::Error; 8 | 9 | #[derive(Copy, Clone, Debug, Error, Display, PartialEq, Eq)] 10 | pub enum IndexError { 11 | /// Index is out of bounds {0} 12 | OutOfBounds(usize), 13 | } 14 | 15 | /// A fragment represents a node's collection of child nodes. 16 | /// 17 | /// Like nodes, fragments are persistent data structures, and you should not mutate them or their 18 | /// content. Rather, you create new instances whenever needed. The API tries to make this easy. 19 | #[derive(Derivative, Deserialize, Eq)] 20 | #[derivative(Debug(bound = ""), Clone(bound = ""), PartialEq(bound = ""))] 21 | #[serde(from = "Vec")] 22 | pub struct Fragment { 23 | inner: Vec, 24 | size: usize, 25 | } 26 | 27 | impl Fragment { 28 | /// An empty fragment 29 | pub const EMPTY: Self = Fragment { 30 | inner: Vec::new(), 31 | size: 0, 32 | }; 33 | /// Reference to an empty fragment 34 | pub const EMPTY_REF: &'static Self = &Self::EMPTY; 35 | 36 | /// Create a new empty fragment 37 | pub fn new() -> Self { 38 | Self::default() 39 | } 40 | 41 | /// The size of the fragment, which is the total of the size of its content nodes. 42 | pub fn size(&self) -> usize { 43 | self.size 44 | } 45 | 46 | /// Get a slice to all child nodes 47 | pub fn children(&self) -> &[S::Node] { 48 | &self.inner[..] 49 | } 50 | 51 | /// The first child of the fragment wrapped in `Some`, or `None` if it is empty. 52 | pub fn first_child(&self) -> Option<&S::Node> { 53 | self.inner.first() 54 | } 55 | 56 | /// The last child of the fragment wrapped in `Some`, or `None` if it is empty. 57 | pub fn last_child(&self) -> Option<&S::Node> { 58 | self.inner.last() 59 | } 60 | 61 | /// The number of child nodes in this fragment. 62 | pub fn child_count(&self) -> usize { 63 | self.inner.len() 64 | } 65 | 66 | /// Create a new fragment containing the combined content of this fragment and the other. 67 | pub fn append(mut self, mut other: Self) -> Self { 68 | if let Some(first) = other.first_child() { 69 | if let Some(last) = self.inner.last_mut() { 70 | if let Some(n1) = last.text_node() { 71 | if let Some(n2) = n1.same_markup(first) { 72 | let mid = n1 73 | .with_text(Text::from(n1.text.as_str().to_owned() + n2.text.as_str())); 74 | *last = S::Node::from(mid); 75 | other.inner.remove(0); 76 | } 77 | } 78 | 79 | self.inner.append(&mut other.inner); 80 | self.size += other.size; 81 | self 82 | } else { 83 | other 84 | } 85 | } else { 86 | self 87 | } 88 | } 89 | 90 | /// Cut out the sub-fragment between the two given positions. 91 | pub fn cut>(&self, range: R) -> Self { 92 | let from = util::from(&range); 93 | let to = util::to(&range, self.size); 94 | 95 | if from == 0 && to == self.size { 96 | return self.clone(); 97 | } 98 | 99 | let mut result = vec![]; 100 | let mut size = 0; 101 | if to > from { 102 | let mut pos = 0; 103 | let mut i = 0; 104 | while pos < to { 105 | let child = &self.inner[i]; 106 | let end = pos + child.node_size(); 107 | if end > from { 108 | let new_child = if pos < from || end > to { 109 | if let Some(node) = child.text_node() { 110 | let len = node.text.len_utf16(); 111 | let start = if from > pos { from - pos } else { 0 }; 112 | let end = usize::min(len, to - pos); 113 | child.cut(start..end) 114 | } else { 115 | let t = pos + 1; 116 | let start = if from > t { from - t } else { 0 }; 117 | let end = usize::min(child.content_size(), to - t); 118 | child.cut(start..end) 119 | } 120 | .into_owned() 121 | } else { 122 | child.clone() 123 | }; 124 | size += new_child.node_size(); 125 | result.push(new_child); 126 | } 127 | pos = end; 128 | i += 1; 129 | } 130 | } 131 | Fragment { 132 | inner: result, 133 | size, 134 | } 135 | } 136 | 137 | /// Invoke a callback for all descendant nodes between the given two positions (relative to 138 | /// start of this fragment). Doesn't descend into a node when the callback returns `false`. 139 | pub fn nodes_between bool>( 140 | &self, 141 | from: usize, 142 | to: usize, 143 | f: &mut F, 144 | node_start: usize, 145 | ) { 146 | let mut pos = 0; 147 | for child in &self.inner { 148 | let end = pos + child.node_size(); 149 | if end > from && f(child, node_start + pos) { 150 | if let Some(content) = child.content() { 151 | let start = pos + 1; 152 | content.nodes_between( 153 | usize::max(0, from - start), 154 | usize::min(content.size(), to - start), 155 | f, 156 | node_start + start, 157 | ) 158 | } 159 | } 160 | pos = end; 161 | } 162 | } 163 | 164 | /// Get all text between positions from and to. When `block_separator` is given, it will be 165 | /// inserted whenever a new block node is started. When `leaf_text` is given, it'll be inserted 166 | /// for every non-text leaf node encountered. 167 | pub fn text_between( 168 | &self, 169 | text: &mut String, 170 | mut separated: bool, 171 | from: usize, 172 | to: usize, 173 | block_separator: Option<&str>, 174 | leaf_text: Option<&str>, 175 | ) { 176 | self.nodes_between( 177 | from, 178 | to, 179 | &mut move |node, pos| { 180 | if let Some(txt_node) = node.text_node() { 181 | let txt = &txt_node.text; 182 | let (rest, skip) = if from > pos { 183 | let skip = from - pos; 184 | (util::split_at_utf16(txt.as_str(), skip).1, skip) 185 | } else { 186 | (txt.as_str(), 0) 187 | }; 188 | 189 | let end = to - pos; 190 | let slice = util::split_at_utf16(rest, end - skip).0; 191 | 192 | text.push_str(slice); 193 | separated = block_separator.is_none(); 194 | } else if node.is_leaf() { 195 | if let Some(leaf_text) = leaf_text { 196 | text.push_str(leaf_text); 197 | } 198 | separated = block_separator.is_none(); 199 | } else if !separated && node.is_block() { 200 | text.push_str(block_separator.unwrap_or("")); 201 | separated = true 202 | } 203 | true 204 | }, 205 | 0, 206 | ) 207 | } 208 | 209 | /// Create a new fragment in which the node at the given index is replaced by the given node. 210 | pub fn replace_child(&self, index: usize, node: S::Node) -> Cow { 211 | let (before, rest) = self.inner.split_at(index); 212 | let (current, after) = rest.split_first().unwrap(); 213 | 214 | if *current == node { 215 | Cow::Borrowed(self) 216 | } else { 217 | let size = self.size + node.node_size() - current.node_size(); 218 | let mut copy = Vec::with_capacity(self.inner.capacity()); 219 | copy.extend_from_slice(before); 220 | copy.push(node); 221 | copy.extend_from_slice(after); 222 | Cow::Owned(Fragment { inner: copy, size }) 223 | } 224 | } 225 | 226 | /// Get the child node at the given index. Panics when the index is out of range. 227 | pub fn child(&self, index: usize) -> &S::Node { 228 | &self.inner[index] 229 | } 230 | 231 | /// Get the child node at the given index, if it exists. 232 | pub fn maybe_child(&self, index: usize) -> Option<&S::Node> { 233 | self.inner.get(index) 234 | } 235 | 236 | pub(crate) fn find_index(&self, pos: usize, round: bool) -> Result { 237 | let len = self.inner.len(); 238 | match pos { 239 | 0 => Ok(Index { 240 | index: 0, 241 | offset: pos, 242 | }), 243 | p if p == self.size => Ok(Index { 244 | index: len, 245 | offset: pos, 246 | }), 247 | p if p > self.size => Err(IndexError::OutOfBounds(p)), 248 | p => { 249 | let mut cur_pos = 0; 250 | for (i, cur) in self.inner.iter().enumerate() { 251 | let end = cur_pos + cur.node_size(); 252 | if end >= p { 253 | if (end == p) || round { 254 | return Ok(Index { 255 | index: i + 1, 256 | offset: end, 257 | }); 258 | } else { 259 | return Ok(Index { 260 | index: i, 261 | offset: cur_pos, 262 | }); 263 | } 264 | } 265 | cur_pos = end; 266 | } 267 | panic!("Invariant failed: self.size must be the sum of all node sizes") 268 | } 269 | } 270 | } 271 | } 272 | 273 | impl Default for Fragment { 274 | fn default() -> Self { 275 | Self { 276 | inner: Vec::new(), 277 | size: 0, 278 | } 279 | } 280 | } 281 | 282 | impl Serialize for Fragment { 283 | fn serialize(&self, serializer: Sr) -> Result 284 | where 285 | Sr: Serializer, 286 | { 287 | self.inner.serialize(serializer) 288 | } 289 | } 290 | 291 | impl From> for Fragment { 292 | fn from(src: Vec) -> Fragment { 293 | let size = src.iter().map(|x| x.node_size()).sum::(); 294 | Fragment { inner: src, size } 295 | } 296 | } 297 | 298 | impl From> for Vec { 299 | fn from(src: Fragment) -> Vec { 300 | src.inner 301 | } 302 | } 303 | 304 | impl From<(A, B)> for Fragment 305 | where 306 | S: Schema, 307 | A: Into, 308 | B: Into, 309 | { 310 | fn from((a, b): (A, B)) -> Self { 311 | Self::from(vec![a.into(), b.into()]) 312 | } 313 | } 314 | 315 | impl From<(A,)> for Fragment 316 | where 317 | N: Node, 318 | S: Schema, 319 | A: Into, 320 | { 321 | fn from((a,): (A,)) -> Self { 322 | Self::from(vec![a.into()]) 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/model/marks.rs: -------------------------------------------------------------------------------- 1 | use super::Schema; 2 | use derivative::Derivative; 3 | use displaydoc::Display; 4 | use serde::{Deserialize, Serialize, Serializer}; 5 | use std::fmt::{self, Debug}; 6 | use std::{borrow::Cow, convert::TryFrom, hash::Hash}; 7 | 8 | /// A set of marks 9 | #[derive(Derivative, Deserialize)] 10 | #[derivative( 11 | Clone(bound = ""), 12 | PartialEq(bound = ""), 13 | Eq(bound = ""), 14 | Default(bound = "") 15 | )] 16 | #[serde(bound = "", try_from = "Vec")] 17 | pub struct MarkSet { 18 | content: Vec, 19 | } 20 | 21 | impl MarkSet { 22 | /// Check whether the set contains this exact mark 23 | pub fn contains(&self, mark: &S::Mark) -> bool { 24 | self.content.contains(mark) 25 | } 26 | 27 | /// Add a mark to the set 28 | pub fn add(&mut self, mark: &S::Mark) { 29 | match self 30 | .content 31 | .binary_search_by_key(&mark.r#type(), Mark::r#type) 32 | { 33 | Ok(index) => { 34 | if &self.content[index] != mark { 35 | self.content[index] = mark.clone(); 36 | } 37 | } 38 | Err(index) => { 39 | self.content.insert(index, mark.clone()); 40 | } 41 | } 42 | } 43 | 44 | /// Remove a mark from the set 45 | pub fn remove(&mut self, mark: &S::Mark) { 46 | match self 47 | .content 48 | .binary_search_by_key(&mark.r#type(), Mark::r#type) 49 | { 50 | Ok(index) => { 51 | self.content.remove(index); 52 | } 53 | Err(_index) => {} 54 | } 55 | } 56 | } 57 | 58 | impl<'a, S: Schema> IntoIterator for &'a MarkSet { 59 | type Item = &'a S::Mark; 60 | type IntoIter = std::slice::Iter<'a, S::Mark>; 61 | fn into_iter(self) -> Self::IntoIter { 62 | self.content.iter() 63 | } 64 | } 65 | 66 | impl Serialize for MarkSet { 67 | fn serialize(&self, serializer: Sr) -> Result 68 | where 69 | Sr: Serializer, 70 | { 71 | self.content.serialize(serializer) 72 | } 73 | } 74 | 75 | #[derive(Display)] 76 | pub enum MarkSetError { 77 | /// Duplicate mark types 78 | Duplicates, 79 | } 80 | 81 | impl TryFrom> for MarkSet { 82 | type Error = MarkSetError; 83 | fn try_from(mut value: Vec) -> Result { 84 | let len = value.len(); 85 | value.sort_by_key(|m| m.r#type()); 86 | value.dedup_by_key(|m| m.r#type()); 87 | if len > value.len() { 88 | Err(MarkSetError::Duplicates) 89 | } else { 90 | Ok(MarkSet { content: value }) 91 | } 92 | } 93 | } 94 | 95 | impl fmt::Debug for MarkSet { 96 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 97 | self.content.fmt(f) 98 | } 99 | } 100 | 101 | /// The methods that 102 | pub trait Mark>: 103 | Serialize + for<'de> Deserialize<'de> + Debug + Clone + PartialEq + Eq + Hash 104 | { 105 | /// The type of this mark. 106 | fn r#type(&self) -> S::MarkType; 107 | 108 | /// Given a set of marks, create a new set which contains this one as well, in the right 109 | /// position. If this mark is already in the set, the set itself is returned. If any marks that 110 | /// are set to be exclusive with this mark are present, those are replaced by this one. 111 | fn add_to_set<'a>(&self, set: Cow<'a, MarkSet>) -> Cow<'a, MarkSet> { 112 | match set 113 | .content 114 | .binary_search_by_key(&self.r#type(), Mark::r#type) 115 | { 116 | Ok(index) => { 117 | if &set.content[index] == self { 118 | set 119 | } else { 120 | let mut owned_set = set.into_owned(); 121 | owned_set.content[index] = self.clone(); 122 | Cow::Owned(owned_set) 123 | } 124 | } 125 | Err(index) => { 126 | let mut owned_set = set.into_owned(); 127 | owned_set.content.insert(index, self.clone()); 128 | Cow::Owned(owned_set) 129 | } 130 | } 131 | } 132 | 133 | /// Remove this mark from the given set, returning a new set. If this mark is not in the set, 134 | /// the set itself is returned. 135 | fn remove_from_set<'a>(&self, set: Cow<'a, MarkSet>) -> Cow<'a, MarkSet> { 136 | match set 137 | .content 138 | .binary_search_by_key(&self.r#type(), Mark::r#type) 139 | { 140 | Ok(index) => { 141 | let mut owned_set = set.into_owned(); 142 | owned_set.content.remove(index); 143 | Cow::Owned(owned_set) 144 | } 145 | Err(_index) => set, 146 | } 147 | } 148 | 149 | /// Create a set with just this mark 150 | fn into_set(self) -> MarkSet { 151 | MarkSet { 152 | content: vec![self], 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | //! # The document model 2 | //! 3 | //! This module is derived from the `prosemirror-markdown` schema and the 4 | //! the general JSON serialization of nodes. 5 | mod content; 6 | mod fragment; 7 | mod marks; 8 | mod node; 9 | mod replace; 10 | mod resolved_pos; 11 | mod schema; 12 | pub(crate) mod util; 13 | 14 | pub use content::{ContentMatch, ContentMatchError}; 15 | pub use fragment::Fragment; 16 | pub use marks::{Mark, MarkSet}; 17 | pub use node::{Node, NodeType, SliceError, Text}; 18 | pub use replace::{InsertError, ReplaceError, Slice}; 19 | pub use resolved_pos::{ResolveErr, ResolvedNode, ResolvedPos}; 20 | pub use schema::{AttrNode, Block, Leaf, MarkType, Schema, TextNode}; 21 | 22 | pub(crate) use replace::replace; 23 | pub(crate) use resolved_pos::Index; 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::{fragment::IndexError, Index, Node, ResolvedNode, ResolvedPos}; 28 | use crate::markdown::{helper::*, ImageAttrs, MarkdownNode, MD}; 29 | use std::fmt::Debug; 30 | use std::ops::Deref; 31 | 32 | #[test] 33 | fn test_null_string() { 34 | assert_eq!( 35 | serde_json::from_str::(r#"{"src": "", "alt": null}"#).unwrap(), 36 | ImageAttrs { 37 | src: String::new(), 38 | title: String::new(), 39 | alt: String::new() 40 | } 41 | ); 42 | } 43 | 44 | #[test] 45 | fn test_deserialize_text() { 46 | assert_eq!( 47 | serde_json::from_str::(r#"{"type": "text", "text": "Foo"}"#).unwrap(), 48 | MarkdownNode::text("Foo"), 49 | ); 50 | } 51 | 52 | #[test] 53 | fn test_size() { 54 | assert_eq!(node("Hello").node_size(), 5); 55 | assert_eq!(node("\u{1F60A}").node_size(), 2); 56 | 57 | let test_3 = p(("Hallo", "Foo")); 58 | assert_eq!(test_3.node_size(), 10); 59 | let ct_3 = test_3.content().unwrap(); 60 | assert_eq!(ct_3.find_index(0, false), Ok(Index::new(0, 0))); 61 | assert_eq!(ct_3.find_index(1, false), Ok(Index::new(0, 0))); 62 | assert_eq!(ct_3.find_index(2, false), Ok(Index::new(0, 0))); 63 | assert_eq!(ct_3.find_index(3, false), Ok(Index::new(0, 0))); 64 | assert_eq!(ct_3.find_index(4, false), Ok(Index::new(0, 0))); 65 | assert_eq!(ct_3.find_index(5, false), Ok(Index::new(1, 5))); 66 | assert_eq!(ct_3.find_index(6, false), Ok(Index::new(1, 5))); 67 | assert_eq!(ct_3.find_index(7, false), Ok(Index::new(1, 5))); 68 | assert_eq!(ct_3.find_index(8, false), Ok(Index::new(2, 8))); 69 | assert_eq!(ct_3.find_index(9, false), Err(IndexError::OutOfBounds(9))); 70 | 71 | assert_eq!( 72 | ResolvedPos::::resolve(&test_3, 0), 73 | Ok(ResolvedPos::new( 74 | 0, 75 | vec![ResolvedNode::new(&test_3, 0, 0)], 76 | 0 77 | )) 78 | ); 79 | } 80 | 81 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 82 | struct Sol<'a> { 83 | node: &'a MarkdownNode, 84 | start: usize, 85 | end: usize, 86 | } 87 | 88 | fn sol(node: &MarkdownNode, start: usize, end: usize) -> Sol { 89 | Sol { node, start, end } 90 | } 91 | 92 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 93 | enum Exp<'a> { 94 | Node(&'a MarkdownNode), 95 | Str(&'static str), 96 | Null, 97 | } 98 | 99 | impl<'a> PartialEq> for Option> { 100 | fn eq(&self, other: &Exp<'a>) -> bool { 101 | if let Some(node) = self { 102 | match other { 103 | Exp::Node(exp_node) => node.deref() == *exp_node, 104 | Exp::Str(text) => &node.text_content() == text, 105 | Exp::Null => false, 106 | } 107 | } else { 108 | *other == Exp::Null 109 | } 110 | } 111 | } 112 | 113 | #[test] 114 | fn test_resolve() { 115 | let test_doc = doc((p(("ab",)), blockquote((p((em("cd"), "ef")),)))); 116 | let _doc = sol(&test_doc, 0, 12); 117 | let _p1 = sol(test_doc.child(0).unwrap(), 1, 3); 118 | let _blk = sol(test_doc.child(1).unwrap(), 5, 11); 119 | let _p2 = sol(_blk.node.child(0).unwrap(), 6, 10); 120 | 121 | let expected = [ 122 | (&[_doc][..], 0, Exp::Null, Exp::Node(_p1.node)), 123 | (&[_doc, _p1], 0, Exp::Null, Exp::Str("ab")), 124 | (&[_doc, _p1], 1, Exp::Str("a"), Exp::Str("b")), 125 | (&[_doc, _p1], 2, Exp::Str("ab"), Exp::Null), 126 | (&[_doc], 4, Exp::Node(_p1.node), Exp::Node(_blk.node)), 127 | (&[_doc, _blk], 0, Exp::Null, Exp::Node(_p2.node)), 128 | (&[_doc, _blk, _p2], 0, Exp::Null, Exp::Str("cd")), 129 | (&[_doc, _blk, _p2], 1, Exp::Str("c"), Exp::Str("d")), 130 | (&[_doc, _blk, _p2], 2, Exp::Str("cd"), Exp::Str("ef")), 131 | (&[_doc, _blk, _p2], 3, Exp::Str("e"), Exp::Str("f")), 132 | (&[_doc, _blk, _p2], 4, Exp::Str("ef"), Exp::Null), 133 | (&[_doc, _blk], 6, Exp::Node(_p2.node), Exp::Null), 134 | (&[_doc], 12, Exp::Node(_blk.node), Exp::Null), 135 | ]; 136 | 137 | for (pos, (path, parent_offset, before, after)) in expected.iter().enumerate() { 138 | let pos = ResolvedPos::::resolve(&test_doc, pos).unwrap(); 139 | assert_eq!(pos.depth, path.len() - 1); 140 | 141 | for (i, exp_i) in path.iter().enumerate() { 142 | let act = sol(pos.node(i), pos.start(i), pos.end(i)); 143 | assert_eq!((i, &act), (i, exp_i)); 144 | if i > 0 { 145 | assert_eq!(pos.before(i), Some(exp_i.start - 1)); 146 | assert_eq!(pos.after(i), Some(exp_i.end + 1)); 147 | } 148 | } 149 | assert_eq!(pos.parent_offset, *parent_offset); 150 | assert_eq!(pos.node_before(), *before); 151 | assert_eq!(pos.node_after(), *after); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/model/node.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | replace, util, ContentMatch, ContentMatchError, Fragment, MarkSet, ReplaceError, ResolveErr, 3 | ResolvedPos, Schema, Slice, TextNode, 4 | }; 5 | use displaydoc::Display; 6 | use serde::{Deserialize, Serialize, Serializer}; 7 | use std::borrow::Cow; 8 | use std::fmt::Debug; 9 | use std::ops::RangeBounds; 10 | use thiserror::Error; 11 | 12 | #[derive(Debug, Clone, Error, Display, Eq, PartialEq)] 13 | /// Error type raised by `Node::slice` when given an invalid replacement. 14 | pub enum SliceError { 15 | /// The given span was invalid 16 | Resolve(#[from] ResolveErr), 17 | /// Unknown 18 | Unknown, 19 | } 20 | 21 | /// This is the type that encodes a kind of node 22 | pub trait NodeType: Copy + Clone + Debug + PartialEq + Eq { 23 | /// ??? 24 | fn compatible_content(self, other: Self) -> bool; 25 | /// ??? 26 | fn valid_content(self, fragment: &Fragment) -> bool; 27 | 28 | /// Check whether the given mark type is allowed in this node. 29 | fn allows_mark_type(self, mark_type: S::MarkType) -> bool; 30 | 31 | /// ??? 32 | fn content_match(self) -> S::ContentMatch; 33 | 34 | /// Check whether the given marks is allowed in this node. 35 | /// 36 | /// By default, marks are allowed in inline nodes and textblocks, i.e. nodes that 37 | /// have inline content. 38 | fn allow_marks(self, marks: &MarkSet) -> bool; 39 | 40 | /// True if this is an inline type. 41 | fn is_inline(self) -> bool { 42 | !self.is_block() 43 | } 44 | /// True if this is a block type 45 | fn is_block(self) -> bool; 46 | } 47 | 48 | /// This class represents a node in the tree that makes up a ProseMirror document. So a document is 49 | /// an instance of Node, with children that are also instances of Node. 50 | pub trait Node + 'static>: 51 | Serialize + for<'de> Deserialize<'de> + Clone + Debug + PartialEq + Eq + Sized + From> 52 | { 53 | /// The size of this node, as defined by the integer-based indexing scheme. For text nodes, 54 | /// this is the amount of characters. For other leaf nodes, it is one. For non-leaf nodes, it 55 | /// is the size of the content plus two (the start and end token). 56 | fn node_size(&self) -> usize { 57 | match self.content() { 58 | Some(c) => c.size() + 2, 59 | None => { 60 | if let Some(node) = self.text_node() { 61 | node.text.len_utf16 62 | } else { 63 | 1 64 | } 65 | } 66 | } 67 | } 68 | 69 | /// The number of children that the node has. 70 | fn child_count(&self) -> usize { 71 | self.content().map_or(0, Fragment::child_count) 72 | } 73 | 74 | /// Get the child node at the given index. Raises an error when the index is out of range. 75 | fn child(&self, index: usize) -> Option<&Self> { 76 | self.content().map(|c| c.child(index)) 77 | } 78 | 79 | /// Get the child node at the given index, if it exists. 80 | fn maybe_child(&self, index: usize) -> Option<&Self> { 81 | self.content().and_then(|c| c.maybe_child(index)) 82 | } 83 | 84 | /// Create a copy of this node, with the given set of marks instead of the node's own marks. 85 | fn mark(&self, marks: MarkSet) -> Self; 86 | 87 | /// Create a copy of this node with only the content between the given positions. 88 | fn cut>(&self, range: R) -> Cow { 89 | let from = util::from(&range); 90 | 91 | if let Some(TextNode { text, marks }) = self.text_node() { 92 | let len = text.len_utf16; 93 | let to = util::to(&range, len); 94 | 95 | if from == 0 && to == len { 96 | return Cow::Borrowed(self); 97 | } 98 | let (_, rest) = util::split_at_utf16(&text.content, from); 99 | let (rest, _) = util::split_at_utf16(rest, to - from); 100 | 101 | Cow::Owned(Self::new_text_node(TextNode { 102 | text: Text::from(rest.to_owned()), 103 | marks: marks.clone(), 104 | })) 105 | } else { 106 | let content_size = self.content_size(); 107 | let to = util::to(&range, content_size); 108 | 109 | if from == 0 && to == content_size { 110 | Cow::Borrowed(self) 111 | } else { 112 | Cow::Owned(self.copy(|c| c.cut(from..to))) 113 | } 114 | } 115 | } 116 | 117 | /// Cut out the part of the document between the given positions, and return it as a `Slice` object. 118 | fn slice + Debug>( 119 | &self, 120 | range: R, 121 | include_parents: bool, 122 | ) -> Result, SliceError> { 123 | let from = util::from(&range); 124 | let to = util::to(&range, self.node_size()); 125 | 126 | if from == to { 127 | return Ok(Slice::default()); 128 | } 129 | 130 | let rp_from = self.resolve(from)?; 131 | let rp_to = self.resolve(to)?; 132 | 133 | let depth = if include_parents { 134 | 0 135 | } else { 136 | rp_from.shared_depth(to) 137 | }; 138 | 139 | let (start, node) = (rp_from.start(depth), rp_from.node(depth)); 140 | let content = if let Some(c) = node.content() { 141 | c.cut(rp_from.pos - start..rp_to.pos - start) 142 | } else { 143 | Fragment::new() 144 | }; 145 | Ok(Slice::new( 146 | content, 147 | rp_from.depth - depth, 148 | rp_to.depth - depth, 149 | )) 150 | } 151 | 152 | /// Replace the part of the document between the given positions with the given slice. The 153 | /// slice must 'fit', meaning its open sides must be able to connect to the surrounding content, 154 | /// and its content nodes must be valid children for the node they are placed into. If any of 155 | /// this is violated, an error of type 156 | /// [`ReplaceError`](#model.ReplaceError) is thrown. 157 | fn replace + Debug>( 158 | &self, 159 | range: R, 160 | slice: &Slice, 161 | ) -> Result> { 162 | let from = util::from(&range); 163 | let to = util::to(&range, self.node_size()); 164 | // FIXME: this max value is my guess, that needs to be tested out 165 | 166 | assert!(to >= from, "replace: {} >= {}", to, from); 167 | 168 | let rp_from = self.resolve(from)?; 169 | let rp_to = self.resolve(to)?; 170 | 171 | let node = replace(&rp_from, &rp_to, slice)?; 172 | Ok(node) 173 | } 174 | 175 | /// Resolve the given position in the document, returning a struct with information about its 176 | /// context. 177 | fn resolve(&self, pos: usize) -> Result, ResolveErr> { 178 | ResolvedPos::resolve(self, pos) 179 | } 180 | 181 | /// Create a new node with the same markup as this node, containing the given content (or 182 | /// empty, if no content is given). 183 | fn copy(&self, map: F) -> Self 184 | where 185 | F: FnOnce(&Fragment) -> Fragment; 186 | 187 | /// Concatenates all the text nodes found in this fragment and its children. 188 | fn text_content(&self) -> String { 189 | if let Some(node) = self.text_node() { 190 | node.text.content.clone() 191 | } else { 192 | let mut buf = String::new(); 193 | if let Some(c) = self.content() { 194 | c.text_between(&mut buf, true, 0, c.size(), Some(""), None); 195 | } 196 | buf 197 | } 198 | } 199 | 200 | /// Returns this node's first child wrapped in `Some`, or `Node` if there are no children. 201 | fn first_child(&self) -> Option<&S::Node> { 202 | self.content().and_then(Fragment::first_child) 203 | } 204 | 205 | /// Represents `.content.size` in JS 206 | fn content_size(&self) -> usize { 207 | self.content().map(Fragment::size).unwrap_or(0) 208 | } 209 | 210 | /// Get the text and marks if this is a text node 211 | fn text_node(&self) -> Option<&TextNode>; 212 | 213 | /// Create a new text node 214 | fn new_text_node(node: TextNode) -> Self; 215 | 216 | /// Creates a new text node 217 | fn text>(text: A) -> Self; 218 | 219 | /// A container holding the node's children. 220 | fn content(&self) -> Option<&Fragment>; 221 | 222 | /// Get the marks on this node 223 | fn marks(&self) -> Option<&MarkSet>; 224 | 225 | /// Get the type of the node 226 | fn r#type(&self) -> S::NodeType; 227 | 228 | /// True when this is a block (non-inline node) 229 | fn is_block(&self) -> bool; 230 | 231 | /// True when this is an inline node (a text node or a node that can appear among text). 232 | fn is_inline(&self) -> bool { 233 | self.r#type().is_inline() 234 | } 235 | 236 | /// True when this is a text node. 237 | fn is_text(&self) -> bool { 238 | self.text_node().is_some() 239 | } 240 | 241 | /// True when this is a leaf node. 242 | fn is_leaf(&self) -> bool { 243 | self.content().is_none() 244 | } 245 | 246 | /// Get the content match in this node at the given index. 247 | fn content_match_at(&self, index: usize) -> Result { 248 | self.r#type() 249 | .content_match() 250 | .match_fragment_range(&self.content().unwrap_or(Fragment::EMPTY_REF), 0..index) 251 | .ok_or(ContentMatchError::InvalidContent) 252 | } 253 | 254 | /// Test whether replacing the range between `from` and `to` (by 255 | /// child index) with the given replacement fragment (which defaults 256 | /// to the empty fragment) would leave the node's content valid. You 257 | /// can optionally pass `start` and `end` indices into the 258 | /// replacement fragment. 259 | fn can_replace>( 260 | &self, 261 | from: usize, 262 | to: usize, 263 | replacement: Option<&Fragment>, 264 | range: R, 265 | ) -> Result { 266 | let replacement = replacement.unwrap_or(Fragment::EMPTY_REF); 267 | let start = util::from(&range); 268 | let end = util::to(&range, replacement.child_count()); 269 | 270 | let one = self 271 | .content_match_at(from)? 272 | .match_fragment_range(&replacement, start..end); 273 | let two = one.and_then(|o| { 274 | o.match_fragment_range(&self.content().unwrap_or(Fragment::EMPTY_REF), to..) 275 | }); 276 | 277 | if matches!(two, Some(m) if m.valid_end()) { 278 | for i in start..end { 279 | if replacement 280 | .child(i) 281 | .marks() 282 | .filter(|m| !self.r#type().allow_marks(m)) 283 | .is_some() 284 | { 285 | return Ok(false); 286 | } 287 | } 288 | Ok(true) 289 | } else { 290 | Ok(false) 291 | } 292 | } 293 | } 294 | 295 | /// A string that stores its length in utf-16 296 | #[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)] 297 | #[serde(from = "String")] 298 | pub struct Text { 299 | len_utf16: usize, 300 | content: String, 301 | } 302 | 303 | impl Text { 304 | /// Return the contained string 305 | pub fn as_str(&self) -> &str { 306 | &self.content 307 | } 308 | 309 | /// The length of this string if it were encoded in utf-16 310 | pub fn len_utf16(&self) -> usize { 311 | self.len_utf16 312 | } 313 | 314 | /// Join two texts together 315 | pub fn join(&self, other: &Self) -> Self { 316 | let left = &self.content; 317 | let right = &other.content; 318 | let mut content = String::with_capacity(left.len() + right.len()); 319 | content.push_str(left); 320 | content.push_str(right); 321 | let len_utf16 = self.len_utf16 + other.len_utf16; 322 | Text { len_utf16, content } 323 | } 324 | 325 | #[cfg(feature = "cmark")] 326 | pub(crate) fn remove_last_newline(&mut self) { 327 | if let Some('\n') = self.content.chars().next_back() { 328 | self.content.pop(); 329 | self.len_utf16 -= 1; 330 | } 331 | } 332 | } 333 | 334 | impl From for Text { 335 | fn from(src: String) -> Text { 336 | Text { 337 | len_utf16: src.encode_utf16().count(), 338 | content: src, 339 | } 340 | } 341 | } 342 | 343 | impl Serialize for Text { 344 | fn serialize(&self, serializer: S) -> Result 345 | where 346 | S: Serializer, 347 | { 348 | self.content.serialize(serializer) 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/model/replace.rs: -------------------------------------------------------------------------------- 1 | use super::{fragment::IndexError, Index}; 2 | use crate::model::{ContentMatchError, Fragment, Node, NodeType, ResolveErr, ResolvedPos, Schema}; 3 | use crate::util::EitherOrBoth; 4 | use derivative::Derivative; 5 | use displaydoc::Display; 6 | use serde::{Deserialize, Serialize}; 7 | use std::borrow::Cow; 8 | use thiserror::Error; 9 | 10 | /// A slice of a fragment 11 | #[derive(Derivative, Deserialize, Serialize)] 12 | #[derivative( 13 | Debug(bound = ""), 14 | PartialEq(bound = ""), 15 | Eq(bound = ""), 16 | Default(bound = "") 17 | )] 18 | #[serde(bound = "", rename_all = "camelCase")] 19 | pub struct Slice { 20 | /// The slice's content. 21 | pub content: Fragment, 22 | /// The open depth at the start. 23 | #[serde(default)] 24 | pub open_start: usize, 25 | /// The open depth at the end. 26 | #[serde(default)] 27 | pub open_end: usize, 28 | } 29 | 30 | impl Slice { 31 | /// Create a slice. When specifying a non-zero open depth, you must 32 | /// make sure that there are nodes of at least that depth at the 33 | /// appropriate side of the fragment — i.e. if the fragment is an empty 34 | /// paragraph node, `openStart` and `openEnd` can't be greater than 1. 35 | /// 36 | /// It is not necessary for the content of open nodes to conform to 37 | /// the schema's content constraints, though it should be a valid 38 | /// start/end/middle for such a node, depending on which sides are 39 | /// open. 40 | pub fn new(content: Fragment, open_start: usize, open_end: usize) -> Slice { 41 | Slice { 42 | content, 43 | open_start, 44 | open_end, 45 | } 46 | } 47 | 48 | pub(crate) fn insert_at( 49 | &self, 50 | pos: usize, 51 | fragment: Fragment, 52 | ) -> Result>, InsertError> { 53 | let content = insert_into(&self.content, pos + self.open_start, fragment, None)?; 54 | Ok(content.map(|c| Slice::::new(c, self.open_start, self.open_end))) 55 | } 56 | } 57 | 58 | /// Error on insertion 59 | #[derive(Debug, Display, Error)] 60 | pub enum InsertError { 61 | /// Index error 62 | Index(#[from] IndexError), 63 | /// Content match error 64 | Content(#[from] ContentMatchError), 65 | } 66 | 67 | fn insert_into( 68 | content: &Fragment, 69 | dist: usize, 70 | insert: Fragment, 71 | parent: Option<&S::Node>, 72 | ) -> Result>, InsertError> { 73 | let Index { index, offset } = content.find_index(dist, false)?; 74 | let child = content.maybe_child(index); 75 | if offset == dist || matches!(child, Some(c) if c.is_text()) { 76 | if let Some(p) = parent { 77 | if !p.can_replace(index, index, Some(&insert), ..)? { 78 | return Ok(None); 79 | } 80 | } 81 | 82 | Ok(Some( 83 | content 84 | .cut(..dist) 85 | .append(insert) 86 | .append(content.cut(dist..)), 87 | )) 88 | } else { 89 | let child = child.unwrap(); // supposed to be safe, because of offset != diff 90 | let inner = insert_into( 91 | child.content().unwrap_or(Fragment::EMPTY_REF), 92 | dist - offset - 1, 93 | insert, 94 | None, 95 | )?; 96 | if let Some(i) = inner { 97 | Ok(Some( 98 | content.replace_child(index, child.copy(|_| i)).into_owned(), 99 | )) 100 | } else { 101 | Ok(None) 102 | } 103 | } 104 | } 105 | 106 | /// An error that can occur when replacing a slice 107 | #[derive(Derivative, Display, Error)] 108 | #[derivative( 109 | Clone(bound = ""), 110 | Debug(bound = ""), 111 | PartialEq(bound = ""), 112 | Eq(bound = "") 113 | )] 114 | pub enum ReplaceError { 115 | /// Inserted content deeper than insertion position 116 | InsertTooDeep, 117 | /// Inconsistent open depths 118 | InconsistentOpenDepths { 119 | /// Depth at the start 120 | from_depth: usize, 121 | /// How many nodes are "open" at the start 122 | open_start: usize, 123 | /// Depth at the end 124 | to_depth: usize, 125 | /// How many nodes are "open" at the end 126 | open_end: usize, 127 | }, 128 | /// Could not resolve an index 129 | Resolve(#[from] ResolveErr), 130 | /// Cannot join {0:?} onto {1:?} 131 | CannotJoin(S::NodeType, S::NodeType), 132 | /// Invalid content for node {0:?} 133 | InvalidContent(S::NodeType), 134 | } 135 | 136 | pub(crate) fn replace( 137 | rp_from: &ResolvedPos, 138 | rp_to: &ResolvedPos, 139 | slice: &Slice, // FIXME: use Cow? 140 | ) -> Result> { 141 | if slice.open_start > rp_from.depth { 142 | Err(ReplaceError::InsertTooDeep) 143 | } else if rp_from.depth - slice.open_start != rp_to.depth - slice.open_end { 144 | Err(ReplaceError::InconsistentOpenDepths { 145 | from_depth: rp_from.depth, 146 | open_start: slice.open_start, 147 | to_depth: rp_to.depth, 148 | open_end: slice.open_end, 149 | }) 150 | } else { 151 | replace_outer(rp_from, rp_to, slice, 0) 152 | } 153 | } 154 | 155 | pub(crate) fn replace_outer( 156 | rp_from: &ResolvedPos, 157 | rp_to: &ResolvedPos, 158 | slice: &Slice, 159 | depth: usize, 160 | ) -> Result> { 161 | let index = rp_from.index(depth); 162 | let node = rp_from.node(depth); 163 | if index == rp_to.index(depth) && depth < rp_from.depth - slice.open_start { 164 | // When both `from` and `to` are in the same child and the we are not at an open node yet 165 | let inner = replace_outer(rp_from, rp_to, slice, depth + 1)?; 166 | Ok(node.copy(|c| c.replace_child(index, inner).into_owned())) 167 | } else if slice.content.size() == 0 { 168 | // When we just delete content, i.e. the replacement slice is empty 169 | let content = replace_two_way(rp_from, &rp_to, depth)?; 170 | close(node, content) 171 | } else if slice.open_start == 0 172 | && slice.open_end == 0 173 | && rp_from.depth == depth 174 | && rp_to.depth == depth 175 | { 176 | // If not parent nodes are open, and the position of both `from` and `to` is at 177 | // the current level: 178 | 179 | // Simple, flat case 180 | let parent = rp_from.parent(); 181 | let content = parent.content().unwrap_or(Fragment::EMPTY_REF); 182 | 183 | let new_content = content 184 | .cut(0..rp_from.parent_offset) 185 | .append(slice.content.clone()) 186 | .append(content.cut(rp_to.parent_offset..)); 187 | close(parent, new_content) 188 | } else { 189 | let (n, start, end) = prepare_slice_for_replace(slice, &rp_from); 190 | let rp_start = n.resolve(start)?; 191 | let rp_end = n.resolve(end)?; 192 | let content = replace_three_way(rp_from, &rp_start, &rp_end, &rp_to, depth)?; 193 | close(node, content) 194 | } 195 | } 196 | 197 | fn check_join(main: &S::Node, sub: &S::Node) -> Result<(), ReplaceError> { 198 | let sub_type = sub.r#type(); 199 | let main_type = main.r#type(); 200 | if sub_type.compatible_content(main_type) { 201 | Ok(()) 202 | } else { 203 | Err(ReplaceError::CannotJoin(sub_type, main_type)) 204 | } 205 | } 206 | 207 | fn joinable<'a, S: Schema>( 208 | rp_before: &ResolvedPos<'a, S>, 209 | rp_after: &ResolvedPos<'a, S>, 210 | depth: usize, 211 | ) -> Result<&'a S::Node, ReplaceError> { 212 | let node = rp_before.node(depth); 213 | check_join::(node, rp_after.node(depth))?; 214 | Ok(node) 215 | } 216 | 217 | fn add_node(child: Cow, target: &mut Vec) { 218 | if let Some(last) = target.last_mut() { 219 | if let Some(c_text) = child.text_node() { 220 | if let Some(l_text) = c_text.same_markup(last) { 221 | let new_text_node = c_text.with_text(l_text.text.join(&c_text.text)); 222 | *last = S::Node::from(new_text_node); 223 | return; 224 | } 225 | } 226 | } 227 | target.push(child.into_owned()); 228 | } 229 | 230 | type Range<'b, 'a, S> = EitherOrBoth<&'b ResolvedPos<'a, S>, &'b ResolvedPos<'a, S>>; 231 | 232 | fn add_range(range: Range, depth: usize, target: &mut Vec) { 233 | let node = range.right_or_left().node(depth); 234 | let mut start_index = 0; 235 | 236 | let end_index = if let Some(rp_end) = range.right() { 237 | rp_end.index(depth) 238 | } else { 239 | node.child_count() 240 | }; 241 | 242 | if let Some(rp_start) = range.left() { 243 | start_index = rp_start.index(depth); 244 | if rp_start.depth > depth { 245 | start_index += 1; 246 | } else if rp_start.text_offset() > 0 { 247 | add_node::(rp_start.node_after().unwrap(), target); 248 | start_index += 1; 249 | } 250 | } 251 | for i in start_index..end_index { 252 | add_node::(Cow::Borrowed(node.child(i).unwrap()), target); 253 | } 254 | if let Some(rp_end) = range.right() { 255 | if rp_end.depth == depth && rp_end.text_offset() > 0 { 256 | add_node::(rp_end.node_before().unwrap(), target); 257 | } 258 | } 259 | } 260 | 261 | fn close(node: &S::Node, content: Fragment) -> Result> { 262 | let node_type = node.r#type(); 263 | if node_type.valid_content(&content) { 264 | Ok(node.copy(|_| content)) 265 | } else { 266 | Err(ReplaceError::InvalidContent(node_type)) 267 | } 268 | } 269 | 270 | fn replace_three_way( 271 | rp_from: &ResolvedPos, 272 | rp_start: &ResolvedPos, 273 | rp_end: &ResolvedPos, 274 | rp_to: &ResolvedPos, 275 | depth: usize, 276 | ) -> Result, ReplaceError> { 277 | let open_start = if rp_from.depth > depth { 278 | Some(joinable(&rp_from, &rp_start, depth + 1)?) 279 | } else { 280 | None 281 | }; 282 | let open_end = if rp_to.depth > depth { 283 | Some(joinable(&rp_end, rp_to, depth + 1)?) 284 | } else { 285 | None 286 | }; 287 | 288 | let mut content = Vec::new(); 289 | add_range(Range::Right(&rp_from), depth, &mut content); 290 | match (open_start, open_end) { 291 | (Some(os), Some(oe)) if rp_start.index(depth) == rp_end.index(depth) => { 292 | check_join(os, oe)?; 293 | let inner = replace_three_way(rp_from, rp_start, rp_end, rp_to, depth + 1)?; 294 | let closed = close(os, inner)?; 295 | add_node::(Cow::Owned(closed), &mut content) 296 | } 297 | _ => { 298 | if let Some(os) = open_start { 299 | let inner = replace_two_way(rp_from, &rp_start, depth + 1)?; 300 | let closed = close(os, inner)?; 301 | add_node::(Cow::Owned(closed), &mut content); 302 | } 303 | add_range(Range::Both(&rp_start, &rp_end), depth, &mut content); 304 | if let Some(oe) = open_end { 305 | let inner = replace_two_way(rp_end, rp_to, depth + 1)?; 306 | let closed = close(oe, inner)?; 307 | add_node::(Cow::Owned(closed), &mut content); 308 | } 309 | } 310 | } 311 | add_range(Range::Left(rp_to), depth, &mut content); 312 | Ok(Fragment::from(content)) 313 | } 314 | 315 | fn replace_two_way( 316 | rp_from: &ResolvedPos, 317 | rp_to: &ResolvedPos, 318 | depth: usize, 319 | ) -> Result, ReplaceError> { 320 | let mut content = Vec::new(); 321 | add_range(Range::Right(rp_from), depth, &mut content); 322 | if rp_from.depth > depth { 323 | let r#type = joinable(rp_from, rp_to, depth + 1)?; 324 | let inner = replace_two_way(rp_from, rp_to, depth + 1)?; 325 | let child = close(r#type, inner)?; 326 | add_node::(Cow::Owned(child), &mut content); 327 | } 328 | add_range(Range::Left(rp_to), depth, &mut content); 329 | Ok(Fragment::from(content)) 330 | } 331 | 332 | fn prepare_slice_for_replace<'a, S: Schema>( 333 | slice: &'a Slice, 334 | rp_along: &ResolvedPos<'a, S>, 335 | ) -> (S::Node, usize, usize) { 336 | let extra = rp_along.depth - slice.open_start; 337 | let parent = rp_along.node(extra); 338 | let mut node = parent.copy(|_| slice.content.clone()); 339 | for i in (0..extra).rev() { 340 | node = rp_along.node(i).copy(|_| Fragment::from((node,))); 341 | } 342 | 343 | let start = slice.open_start + extra; 344 | let end = node.content_size() - slice.open_end - extra; 345 | (node, start, end) 346 | } 347 | 348 | #[cfg(test)] 349 | mod tests { 350 | use super::ReplaceError; 351 | use crate::markdown::helper::{blockquote, doc, h1, li, p, ul}; 352 | use crate::markdown::{MarkdownNode, MarkdownNodeType, MD}; 353 | use crate::model::{Fragment, Node, Slice, SliceError}; 354 | use displaydoc::Display; 355 | use std::fmt::Debug; 356 | use std::ops::{Range, RangeBounds}; 357 | use thiserror::Error; 358 | 359 | #[derive(Debug, Display, Error, PartialEq, Eq)] 360 | enum RplError { 361 | /// Could not slice 362 | Slice(#[from] SliceError), 363 | } 364 | 365 | // insert.slice(insert.tag.a, insert.tag.b) 366 | fn rpl + Debug, SR: RangeBounds + Debug>( 367 | (doc, range): (MarkdownNode, DR), 368 | insert: Option<(MarkdownNode, SR)>, 369 | expected: MarkdownNode, 370 | ) -> Result<(), RplError> { 371 | let slice = insert 372 | .map(|(n, r)| n.slice(r, false).unwrap()) 373 | .unwrap_or_default(); 374 | 375 | assert_eq!(doc.replace(range, &slice), Ok(expected)); 376 | Ok(()) 377 | } 378 | 379 | #[test] 380 | fn join_on_delete() { 381 | let t1: MarkdownNode = doc((p("one"), p("two"))); 382 | let e1: MarkdownNode = doc((p("onwo"),)); 383 | rpl::<_, Range>((t1, 3..7), None, e1).unwrap(); 384 | } 385 | 386 | #[test] 387 | fn merges_matching_blocks() { 388 | let t2: MarkdownNode = doc((p("one"), p("two"))); 389 | let i2: MarkdownNode = doc((p("xxxx"), p("yyyy"))); 390 | 391 | let f2: Fragment = Fragment::from(vec![p("xx"), p("yy")]); 392 | assert_eq!(i2.slice(3..9, false), Ok(Slice::new(f2, 1, 1))); 393 | 394 | let e2: MarkdownNode = doc((p("onxx"), p("yywo"))); 395 | rpl((t2, 3..7), Some((i2, 3..9)), e2).unwrap(); 396 | } 397 | 398 | #[test] 399 | fn merges_when_adding_text() { 400 | let (t3, r3) = (doc((p("one"), p("two"))), 3..7); 401 | let (i3, s3) = (doc((p("H"),)), 1..2); 402 | let e3 = doc((p("onHwo"),)); 403 | 404 | rpl((t3, r3), Some((i3, s3)), e3).unwrap(); 405 | } 406 | 407 | #[test] 408 | fn can_insert_text() { 409 | let t4 = doc(vec![p("before"), p("one"), p("after")]); 410 | let r4 = 11..11; 411 | 412 | let i4 = doc(vec![p("H")]); 413 | let s4 = 1..2; 414 | 415 | let e4 = doc(vec![p("before"), p("onHe"), p("after")]); 416 | rpl((t4, r4), Some((i4, s4)), e4).unwrap(); 417 | } 418 | 419 | #[test] 420 | fn doesnt_merge_non_matching_blocks() { 421 | // What does this test? The name seems to contradict that it succeeds 422 | // This is from the original prosemirror test cases 423 | 424 | let t5 = doc(vec![p("one"), p("two")]); 425 | let r5 = 3..7; 426 | 427 | let i5 = doc(vec![h1("H")]); 428 | let s5 = 1..2; 429 | 430 | let e5 = doc(vec![p("onHwo")]); 431 | rpl((t5, r5), Some((i5, s5)), e5).unwrap(); 432 | } 433 | 434 | #[test] 435 | fn can_merge_a_nested_node() { 436 | let t6 = doc(blockquote(blockquote(vec![p("one"), p("two")]))); 437 | let i6 = doc(p("H")); 438 | let e6 = doc(blockquote(blockquote(p("onHwo")))); 439 | 440 | rpl((t6, 5..9), Some((i6, 1..2)), e6).unwrap(); 441 | } 442 | 443 | #[test] 444 | fn can_replace_within_a_block() { 445 | let t = doc(blockquote(p("abcd"))); 446 | let i = doc(p("xyz")); 447 | let e = doc(blockquote(p("ayd"))); 448 | 449 | rpl((t, 3..5), Some((i, 2..3)), e).unwrap(); 450 | } 451 | 452 | #[test] 453 | fn can_insert_a_lopsided_slice() { 454 | let t = doc(blockquote(blockquote(vec![p("one"), p("two"), p("three")]))); //5..12 455 | let i = doc(blockquote(vec![p("aaaa"), p("bb"), p("cc"), p("dd")])); //4..15 456 | let e = doc(blockquote(blockquote(vec![ 457 | p("onaa"), 458 | p("bb"), 459 | p("cc"), 460 | p("three"), 461 | ]))); 462 | 463 | rpl((t, 5..12), Some((i, 4..15)), e).unwrap(); 464 | } 465 | 466 | #[test] 467 | fn can_insert_a_deep_lopsided_slice() { 468 | let t = doc(blockquote(vec![ 469 | blockquote(vec![p("one"), p("two"), p("three")]), 470 | p("x"), 471 | ])); //5..20 472 | let i = doc(vec![blockquote(vec![p("aaaa"), p("bb"), p("cc")]), p("dd")]); // 4..16 473 | let e = doc(blockquote(vec![ 474 | blockquote(vec![p("onaa"), p("bb"), p("cc")]), 475 | p("x"), 476 | ])); 477 | 478 | rpl((t, 5..20), Some((i, 4..16)), e).unwrap(); 479 | } 480 | 481 | #[test] 482 | fn can_merge_multiple_levels() { 483 | let t = doc(vec![ 484 | blockquote(blockquote(p("hello"))), 485 | blockquote(blockquote(p("a"))), 486 | ]); 487 | let e = doc(blockquote(blockquote(p("hella")))); 488 | 489 | rpl::<_, Range>((t, 7..14), None, e).unwrap(); 490 | } 491 | 492 | #[test] 493 | fn can_merge_multiple_levels_while_inserting() { 494 | let t = doc(vec![ 495 | blockquote(blockquote(p("hello"))), 496 | blockquote(blockquote(p("a"))), 497 | ]); 498 | let i = doc(p("i")); 499 | let e = doc(blockquote(blockquote(p("hellia")))); 500 | 501 | rpl((t, 7..14), Some((i, 1..2)), e).unwrap(); 502 | } 503 | 504 | #[test] 505 | fn can_insert_a_split() { 506 | let t = doc(p("foobar")); // 4..4 507 | let i = doc(vec![p("x"), p("y")]); // 1..5 508 | let e = doc(vec![p("foox"), p("ybar")]); 509 | 510 | rpl((t, 4..4), Some((i, 1..5)), e).unwrap(); 511 | } 512 | 513 | #[test] 514 | fn can_insert_a_deep_split() { 515 | let t = doc(blockquote(p("fooxbar"))); 516 | let i = doc(vec![blockquote(p("x")), blockquote(p("y"))]); 517 | let e = doc(vec![blockquote(p("foox")), blockquote(p("ybar"))]); 518 | 519 | rpl((t, 5..6), Some((i, 2..8)), e).unwrap(); 520 | } 521 | 522 | #[test] 523 | fn can_add_a_split_one_level_up() { 524 | let t = doc(blockquote(vec![p("foou"), p("vbar")])); 525 | let i = doc(vec![blockquote(p("x")), blockquote(p("y"))]); 526 | let e = doc(vec![blockquote(p("foox")), blockquote(p("ybar"))]); 527 | 528 | rpl((t, 5..9), Some((i, 2..8)), e).unwrap(); 529 | } 530 | 531 | #[test] 532 | fn keeps_the_node_type_of_the_left_node() { 533 | let t = doc(h1("foobar")); 534 | let i = doc(p("foobaz")); 535 | let e = doc(h1("foobaz")); 536 | 537 | rpl((t, 4..8), Some((i, 4..8)), e).unwrap(); 538 | } 539 | 540 | #[test] 541 | fn keeps_the_node_type_even_when_empty() { 542 | let t = doc(h1("bar")); 543 | let i = doc(p("foobaz")); 544 | let e = doc(h1("baz")); 545 | 546 | rpl((t, 1..5), Some((i, 4..8)), e).unwrap(); 547 | } 548 | 549 | fn bad + Debug, SR: RangeBounds + Debug>( 550 | (doc, range): (MarkdownNode, DR), 551 | insert: Option<(MarkdownNode, SR)>, 552 | pattern: ReplaceError, 553 | ) { 554 | let slice = insert 555 | .map(|(n, r)| n.slice(r, false).unwrap()) 556 | .unwrap_or_default(); 557 | assert_eq!(doc.replace(range, &slice), Err(pattern)); 558 | } 559 | 560 | #[test] 561 | fn doesnt_allow_the_left_side_to_be_too_deep() { 562 | let t = doc(p("")); // 1..1 563 | let i = doc(blockquote(p(""))); // 2..4 564 | bad((t, 1..1), Some((i, 2..4)), ReplaceError::InsertTooDeep); 565 | } 566 | 567 | #[test] 568 | fn doesnt_allow_a_depth_mismatch() { 569 | let t = doc(p("")); // 1..1 570 | let i = doc(p("")); // 0..1 571 | bad( 572 | (t, 1..1), 573 | Some((i, 0..1)), 574 | ReplaceError::InconsistentOpenDepths { 575 | from_depth: 1, 576 | open_start: 0, 577 | to_depth: 1, 578 | open_end: 1, 579 | }, 580 | ); 581 | } 582 | 583 | #[test] 584 | fn rejects_a_bad_fit() { 585 | let t = doc(vec![]); // 0..0 586 | let i = doc(p("foo")); // 1..4 587 | let e = ReplaceError::InvalidContent(MarkdownNodeType::Doc); 588 | 589 | bad((t, 0..0), Some((i, 1..4)), e); 590 | } 591 | 592 | #[test] 593 | fn rejects_unjoinable_content() { 594 | let t = doc(ul(li(p("a")))); // 6..7 595 | let i = doc(p("foo")); //4..5 596 | let e = ReplaceError::CannotJoin(MarkdownNodeType::Paragraph, MarkdownNodeType::BulletList); 597 | 598 | bad((t, 6..7), Some((i, 4..5)), e); 599 | } 600 | 601 | #[test] 602 | fn rejects_an_unjoinable_delete() { 603 | let t = doc(vec![blockquote(p("a")), ul(li(p("b")))]); //4..6 604 | let e = 605 | ReplaceError::CannotJoin(MarkdownNodeType::BulletList, MarkdownNodeType::Blockquote); 606 | 607 | bad::<_, Range>((t, 4..6), None, e); 608 | } 609 | 610 | #[test] 611 | fn check_content_validity() { 612 | let t = doc(blockquote(p("hi"))); // 1..6 613 | let i = doc(blockquote("hi")); // 3..4 614 | let e = ReplaceError::InvalidContent(MarkdownNodeType::Blockquote); 615 | 616 | bad((t, 1..6), Some((i, 3..4)), e); 617 | } 618 | } 619 | -------------------------------------------------------------------------------- /src/model/resolved_pos.rs: -------------------------------------------------------------------------------- 1 | use super::{fragment::IndexError, Fragment, Node, Schema}; 2 | use derivative::Derivative; 3 | use displaydoc::Display; 4 | use std::borrow::Cow; 5 | use std::fmt; 6 | use thiserror::Error; 7 | 8 | /// Errors at `resolve` 9 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Display, Error)] 10 | pub enum ResolveErr { 11 | /// Position {pos} out of range 12 | RangeError { 13 | /// The position that was out of range 14 | pos: usize, 15 | }, 16 | /// Index error 17 | Index(#[from] IndexError), 18 | } 19 | 20 | #[derive(Derivative, new)] 21 | #[derivative(PartialEq(bound = ""), Eq(bound = ""))] 22 | /// A node in the resolution path 23 | pub struct ResolvedNode<'a, S: Schema> { 24 | /// Reference to the node 25 | pub node: &'a S::Node, 26 | /// Index of the in the parent fragment 27 | pub index: usize, 28 | /// Offset immediately before the node 29 | pub before: usize, 30 | } 31 | 32 | impl<'a, S: Schema> Clone for ResolvedNode<'a, S> { 33 | fn clone(&self) -> Self { 34 | Self { 35 | node: self.node, 36 | index: self.index, 37 | before: self.before, 38 | } 39 | } 40 | } 41 | 42 | impl<'a, S: Schema> fmt::Debug for ResolvedNode<'a, S> { 43 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | f.debug_struct("ResolvedNode") 45 | .field("node.type", &self.node.r#type()) 46 | .field("index", &self.index) 47 | .field("before", &self.before) 48 | .finish() 49 | } 50 | } 51 | 52 | /// You can resolve a position to get more information about it. Objects of this class represent 53 | /// such a resolved position, providing various pieces of context information, and some helper 54 | /// methods. 55 | #[derive(Derivative)] 56 | #[derivative( 57 | Debug(bound = ""), 58 | Clone(bound = ""), 59 | PartialEq(bound = ""), 60 | Eq(bound = "") 61 | )] 62 | pub struct ResolvedPos<'a, S: Schema> { 63 | pub(crate) pos: usize, 64 | path: Vec>, 65 | pub(crate) parent_offset: usize, 66 | pub(crate) depth: usize, 67 | } 68 | 69 | impl<'a, S: Schema> ResolvedPos<'a, S> { 70 | pub(crate) fn new(pos: usize, path: Vec>, parent_offset: usize) -> Self { 71 | Self { 72 | depth: path.len() - 1, 73 | pos, 74 | path, 75 | parent_offset, 76 | } 77 | } 78 | 79 | /// The parent node that the position points into. Note that even if 80 | /// a position points into a text node, that node is not considered 81 | /// the parent—text nodes are ‘flat’ in this model, and have no content. 82 | pub fn parent(&self) -> &S::Node { 83 | self.node(self.depth) 84 | } 85 | 86 | /// The root node in which the position was resolved. 87 | pub fn doc(&self) -> &S::Node { 88 | self.node(0) 89 | } 90 | 91 | /// The ancestor node at the given level. `p.node(p.depth)` is the same as `p.parent()`. 92 | pub fn node(&self, depth: usize) -> &'a S::Node { 93 | self.path[depth].node 94 | } 95 | 96 | /// The index into the ancestor at the given level. If this points at the 3rd node in the 97 | /// 2nd paragraph on the top level, for example, `p.index(0)` is 1 and `p.index(1)` is 2. 98 | pub fn index(&self, depth: usize) -> usize { 99 | self.path[depth].index 100 | } 101 | 102 | /// The index pointing after this position into the ancestor at the given level. 103 | pub fn index_after(&self, depth: usize) -> usize { 104 | let index = self.index(depth); 105 | if depth == self.depth && self.text_offset() == 0 { 106 | index 107 | } else { 108 | index + 1 109 | } 110 | } 111 | 112 | /// The (absolute) position at the start of the node at the given level. 113 | pub fn start(&self, depth: usize) -> usize { 114 | if depth == 0 { 115 | 0 116 | } else { 117 | self.path[depth - 1].before + 1 118 | } 119 | } 120 | 121 | /// The (absolute) position at the end of the node at the given level. 122 | pub fn end(&self, depth: usize) -> usize { 123 | self.start(depth) + self.node(depth).content().map(Fragment::size).unwrap_or(0) 124 | } 125 | 126 | /// The (absolute) position directly before the wrapping node at the given level, or, when 127 | /// depth is `self.depth + 1`, the original position. 128 | pub fn before(&self, depth: usize) -> Option { 129 | if depth == 0 { 130 | None 131 | } else if depth == self.depth + 1 { 132 | Some(self.pos) 133 | } else { 134 | Some(self.path[depth - 1].before) 135 | } 136 | } 137 | 138 | /// The (absolute) position directly after the wrapping node at the given level, or the 139 | /// original position when depth is `self.depth + 1`. 140 | pub fn after(&self, depth: usize) -> Option { 141 | if depth == 0 { 142 | None 143 | } else if depth == self.depth + 1 { 144 | Some(self.pos) 145 | } else { 146 | Some(self.path[depth - 1].before + self.path[depth].node.node_size()) 147 | } 148 | } 149 | 150 | /// When this position points into a text node, this returns the 151 | /// distance between the position and the start of the text node. 152 | /// Will be zero for positions that point between nodes. 153 | pub fn text_offset(&self) -> usize { 154 | self.pos - self.path.last().unwrap().before 155 | } 156 | 157 | /// Get the node directly before the position, if any. If the position points into a text node, 158 | /// only the part of that node before the position is returned. 159 | pub fn node_before(&self) -> Option> { 160 | let index = self.index(self.depth); 161 | let d_off = self.pos - self.path.last().unwrap().before; 162 | if d_off > 0 { 163 | let parent = self.parent(); 164 | let child = parent.child(index).unwrap(); 165 | let cut = child.cut(0..d_off); 166 | Some(cut) 167 | } else if index == 0 { 168 | None 169 | } else { 170 | Some(Cow::Borrowed(self.parent().child(index - 1).unwrap())) 171 | } 172 | } 173 | 174 | /// Get the node directly after the position, if any. If the position points into a text node, 175 | /// only the part of that node after the position is returned. 176 | pub fn node_after(&self) -> Option> { 177 | let parent = self.parent(); 178 | let index = self.index(self.depth); 179 | if index == parent.child_count() { 180 | return None; 181 | } 182 | let d_off = self.pos - self.path.last().unwrap().before; 183 | let child = parent.child(index).unwrap(); 184 | if d_off > 0 { 185 | Some(child.cut(d_off..)) 186 | } else { 187 | Some(Cow::Borrowed(child)) 188 | } 189 | } 190 | 191 | /// The depth up to which this position and the given (non-resolved) 192 | /// position share the same parent nodes. 193 | pub fn shared_depth(&self, pos: usize) -> usize { 194 | for depth in (1..=self.depth).rev() { 195 | if self.start(depth) <= pos && self.end(depth) >= pos { 196 | return depth; 197 | } 198 | } 199 | 0 200 | } 201 | 202 | pub(crate) fn resolve(doc: &'a S::Node, pos: usize) -> Result { 203 | if pos > doc.content().unwrap().size() { 204 | return Err(ResolveErr::RangeError { pos }); 205 | } 206 | let mut path = vec![]; 207 | let mut start = 0; 208 | let mut parent_offset = pos; 209 | let mut node = doc; 210 | 211 | loop { 212 | let Index { index, offset } = node 213 | .content() 214 | .unwrap_or(&Fragment::default()) 215 | .find_index(parent_offset, false)?; 216 | let rem = parent_offset - offset; 217 | path.push(ResolvedNode { 218 | node, 219 | index, 220 | before: start + offset, 221 | }); 222 | if rem == 0 { 223 | break; 224 | } 225 | node = node.child(index).unwrap(); 226 | if node.is_text() { 227 | break; 228 | } 229 | parent_offset = rem - 1; 230 | start += offset + 1; 231 | } 232 | Ok(ResolvedPos::new(pos, path, parent_offset)) 233 | } 234 | } 235 | 236 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 237 | pub(crate) struct Index { 238 | pub index: usize, 239 | pub offset: usize, 240 | } 241 | 242 | impl Index { 243 | #[allow(unused)] 244 | pub fn new(index: usize, offset: usize) -> Index { 245 | Index { index, offset } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/model/schema.rs: -------------------------------------------------------------------------------- 1 | use super::{ContentMatch, Fragment, Mark, MarkSet, Node, NodeType, Text}; 2 | use derivative::Derivative; 3 | use serde::{Deserialize, Serialize}; 4 | use std::fmt::Debug; 5 | 6 | /// This type represents a schema. 7 | pub trait Schema: Sized + 'static { 8 | /// This type represents any of the marks that are valid in the schema. 9 | type Mark: Mark; 10 | /// This type represents any of the mark types that are valid in the schema. 11 | type MarkType: MarkType; 12 | /// This type represents any of the nodes that are valid in the schema. 13 | type Node: Node; 14 | /// This type represents any of the node types that are valid in the schema. 15 | type NodeType: NodeType; 16 | /// This type represents the `ContentMatch` impl 17 | type ContentMatch: ContentMatch; 18 | } 19 | 20 | /// A simple block node 21 | #[derive(Derivative, Deserialize, Serialize)] 22 | #[derivative( 23 | Debug(bound = ""), 24 | Clone(bound = ""), 25 | Default(bound = ""), 26 | PartialEq(bound = ""), 27 | Eq(bound = "") 28 | )] 29 | #[serde(bound = "")] 30 | pub struct Block { 31 | /// The content. 32 | #[serde(default)] 33 | #[derivative(Debug(bound = ""))] 34 | pub content: Fragment, 35 | } 36 | 37 | impl Block { 38 | /// Copies this block, mapping the content 39 | pub fn copy(&self, map: F) -> Self 40 | where 41 | F: FnOnce(&Fragment) -> Fragment, 42 | { 43 | Block { 44 | content: map(&self.content), 45 | } 46 | } 47 | } 48 | 49 | /// A node with attributes 50 | #[derive(Derivative, Deserialize, Serialize)] 51 | #[derivative( 52 | Debug(bound = "A: Debug"), 53 | Clone(bound = "A: Clone"), 54 | Default(bound = "A: Default"), 55 | PartialEq(bound = "A: PartialEq"), 56 | Eq(bound = "A: Eq") 57 | )] 58 | #[serde(bound = "A: for<'d> Deserialize<'d> + Serialize")] 59 | pub struct AttrNode { 60 | /// Attributes 61 | pub attrs: A, 62 | 63 | /// The content. 64 | #[serde(default)] 65 | #[derivative(Debug(bound = ""))] 66 | pub content: Fragment, 67 | } 68 | 69 | impl AttrNode { 70 | /// Copies this block, mapping the content 71 | pub fn copy(&self, map: F) -> Self 72 | where 73 | F: FnOnce(&Fragment) -> Fragment, 74 | { 75 | AttrNode { 76 | content: map(&self.content), 77 | attrs: self.attrs.clone(), 78 | } 79 | } 80 | } 81 | 82 | /// A text node 83 | #[derive(Derivative, Deserialize, Serialize)] 84 | #[derivative( 85 | Debug(bound = ""), 86 | Clone(bound = ""), 87 | Default(bound = ""), 88 | PartialEq(bound = ""), 89 | Eq(bound = "") 90 | )] 91 | #[serde(bound = "")] 92 | pub struct TextNode { 93 | // todo: replace with typemap 94 | /// Marks on this node 95 | #[serde(default)] 96 | pub marks: MarkSet, 97 | /// The actual text 98 | pub text: Text, 99 | } 100 | 101 | impl TextNode { 102 | /// Check whether the marks are identical 103 | pub fn same_markup<'o>(&self, other: &'o S::Node) -> Option<&'o TextNode> { 104 | other.text_node().filter(|x| x.marks == self.marks) 105 | } 106 | 107 | /// Create a new `TextNode` with the given text 108 | pub fn with_text(&self, text: Text) -> Self { 109 | TextNode { 110 | marks: self.marks.clone(), 111 | text, 112 | } 113 | } 114 | } 115 | 116 | /// A leaf node (just attributes) 117 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 118 | pub struct Leaf { 119 | /// Attributes 120 | pub attrs: A, 121 | } 122 | 123 | /// Like nodes, marks (which are associated with nodes to signify 124 | /// things like emphasis or being part of a link) are 125 | /// [tagged](#model.Mark.type) with type objects, which are 126 | /// instantiated once per `Schema`. 127 | pub trait MarkType: Copy + Clone + Debug + PartialEq + Eq + PartialOrd + Ord {} 128 | -------------------------------------------------------------------------------- /src/model/util.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Bound, RangeBounds}; 2 | 3 | pub fn from>(range: &R) -> usize { 4 | match range.start_bound() { 5 | Bound::Unbounded => 0, 6 | Bound::Included(x) => *x, 7 | Bound::Excluded(x) => x + 1, 8 | } 9 | } 10 | 11 | pub fn to>(range: &R, max: usize) -> usize { 12 | match range.end_bound() { 13 | Bound::Unbounded => max, 14 | Bound::Included(x) => x + 1, 15 | Bound::Excluded(x) => *x, 16 | } 17 | } 18 | 19 | pub fn split_at_utf16(text: &str, mut index: usize) -> (&str, &str) { 20 | let mut iter = text.chars(); 21 | while index > 0 { 22 | if let Some(c) = iter.next() { 23 | let l = c.len_utf16(); 24 | if l > index { 25 | panic!("Can't split in the middle of a character") 26 | } else { 27 | index -= l; 28 | } 29 | } else { 30 | return (text, ""); 31 | } 32 | } 33 | let mid = text.len() - iter.as_str().len(); 34 | text.split_at(mid) 35 | } 36 | -------------------------------------------------------------------------------- /src/transform/mark_step.rs: -------------------------------------------------------------------------------- 1 | use super::{util::Span, StepKind}; 2 | use crate::model::{Fragment, Mark, MarkSet, Node, NodeType, Schema, Slice}; 3 | use derivative::Derivative; 4 | use serde::{Deserialize, Serialize}; 5 | use std::borrow::Cow; 6 | 7 | fn map_fragment_parent(fragment: &Fragment, f: &F, parent: &S::Node) -> Fragment 8 | where 9 | F: Fn(S::Node, &S::Node, usize) -> S::Node, 10 | { 11 | let mut mapped = vec![]; 12 | for (i, child) in fragment.children().iter().enumerate() { 13 | let mut child = child.copy(|c| map_fragment_parent(c, f, &*child)); 14 | 15 | if child.is_inline() { 16 | child = f(child, parent, i) 17 | } 18 | mapped.push(child) 19 | } 20 | Fragment::from(mapped) 21 | } 22 | 23 | fn map_fragment(fragment: &Fragment, f: &F) -> Fragment 24 | where 25 | F: Fn(S::Node) -> S::Node, 26 | { 27 | let mut mapped = vec![]; 28 | for child in fragment.children() { 29 | let mut child = child.copy(|c| map_fragment(c, f)); 30 | 31 | if child.is_inline() { 32 | child = f(child) 33 | } 34 | mapped.push(child) 35 | } 36 | Fragment::from(mapped) 37 | } 38 | 39 | /// Adding a mark on some part of the document 40 | #[derive(Derivative, Deserialize, Serialize)] 41 | #[derivative(Debug(bound = ""), PartialEq(bound = ""), Eq(bound = ""))] 42 | #[serde(bound = "", rename_all = "camelCase")] 43 | pub struct AddMarkStep { 44 | /// The affected part of the document 45 | #[serde(flatten)] 46 | pub span: Span, 47 | /// The mark to add 48 | pub mark: S::Mark, 49 | } 50 | 51 | /// Removing a mark on some part of the document 52 | #[derive(Derivative, Deserialize, Serialize)] 53 | #[derivative(Debug(bound = ""), PartialEq(bound = ""), Eq(bound = ""))] 54 | #[serde(bound = "", rename_all = "camelCase")] 55 | pub struct RemoveMarkStep { 56 | /// The affected part of the document 57 | #[serde(flatten)] 58 | pub span: Span, 59 | /// The mark to remove 60 | pub mark: S::Mark, 61 | } 62 | 63 | impl StepKind for AddMarkStep { 64 | fn apply(&self, doc: &S::Node) -> super::StepResult { 65 | let old_slice = doc.slice(self.span.from..self.span.to, false)?; 66 | let rp_from = doc.resolve(self.span.from)?; 67 | let parent = rp_from.node(rp_from.shared_depth(self.span.to)); 68 | 69 | let new_content = map_fragment_parent( 70 | &old_slice.content, 71 | &|node, parent, _i| { 72 | if parent.r#type().allows_mark_type(self.mark.r#type()) { 73 | let new_marks = node.marks().map(Cow::Borrowed).unwrap_or_default(); 74 | node.mark(self.mark.add_to_set(new_marks).into_owned()) 75 | } else { 76 | node 77 | } 78 | }, 79 | parent, 80 | ); 81 | 82 | let slice = Slice::new(new_content, old_slice.open_start, old_slice.open_end); 83 | // TODO: Cow::Owned? 84 | let new_node = doc.replace(self.span.from..self.span.to, &slice)?; 85 | Ok(new_node) 86 | } 87 | } 88 | 89 | impl StepKind for RemoveMarkStep { 90 | fn apply(&self, doc: &S::Node) -> super::StepResult { 91 | let old_slice = doc.slice(self.span.from..self.span.to, false)?; 92 | 93 | let new_content = map_fragment(&old_slice.content, &|node| { 94 | let new_marks: Cow> = node.marks().map(Cow::Borrowed).unwrap_or_default(); 95 | node.mark(self.mark.remove_from_set(new_marks).into_owned()) 96 | }); 97 | 98 | let slice = Slice::new(new_content, old_slice.open_start, old_slice.open_end); 99 | let new_node = doc.replace(self.span.from..self.span.to, &slice)?; 100 | Ok(new_node) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/transform/mod.rs: -------------------------------------------------------------------------------- 1 | //! # The document transformations 2 | //! 3 | mod mark_step; 4 | mod replace_step; 5 | mod step; 6 | mod util; 7 | 8 | pub use mark_step::{AddMarkStep, RemoveMarkStep}; 9 | pub use replace_step::{ReplaceAroundStep, ReplaceStep}; 10 | pub use step::{StepError, StepKind, StepResult}; 11 | pub use util::Span; 12 | 13 | use crate::model::Schema; 14 | use derivative::Derivative; 15 | use serde::{Deserialize, Serialize}; 16 | 17 | /// A list of steps 18 | #[allow(type_alias_bounds)] 19 | pub type Steps = Vec>; 20 | 21 | /// Steps that can be applied on a document 22 | #[derive(Derivative, Deserialize, Serialize)] 23 | #[derivative(Debug(bound = ""), PartialEq(bound = ""), Eq(bound = ""))] 24 | #[serde(bound = "", tag = "stepType", rename_all = "camelCase")] 25 | pub enum Step { 26 | /// Replace some content 27 | Replace(ReplaceStep), 28 | /// Replace around some content 29 | ReplaceAround(ReplaceAroundStep), 30 | /// Add a mark to a span 31 | AddMark(AddMarkStep), 32 | /// Remove a mark from a span 33 | RemoveMark(RemoveMarkStep), 34 | } 35 | 36 | impl Step { 37 | /// Apply the step to the given node 38 | pub fn apply(&self, doc: &S::Node) -> StepResult { 39 | match self { 40 | Self::Replace(r_step) => r_step.apply(doc), 41 | Self::ReplaceAround(ra_step) => ra_step.apply(doc), 42 | Self::AddMark(am_step) => am_step.apply(doc), 43 | Self::RemoveMark(rm_step) => rm_step.apply(doc), 44 | } 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::{AddMarkStep, ReplaceStep, Span, Step, StepKind}; 51 | use crate::markdown::{ 52 | helper::{doc, node, p, strong}, 53 | MarkdownMark, MarkdownNode, MD, 54 | }; 55 | use crate::model::{Fragment, Node, Slice}; 56 | 57 | #[test] 58 | fn test_apply() { 59 | let d1 = doc(p("Hello World!")); 60 | let step1 = AddMarkStep:: { 61 | span: Span { from: 1, to: 9 }, 62 | mark: MarkdownMark::Strong, 63 | }; 64 | let d2 = step1.apply(&d1).unwrap(); 65 | assert_eq!(d2, doc(p(vec![strong("Hello Wo"), node("rld!")]))); 66 | } 67 | 68 | #[test] 69 | fn test_deserialize() { 70 | let s1: Step = serde_json::from_str( 71 | r#"{"stepType":"addMark","mark":{"type":"em"},"from":61,"to":648}"#, 72 | ) 73 | .unwrap(); 74 | 75 | assert_eq!( 76 | s1, 77 | Step::AddMark(AddMarkStep { 78 | span: Span { from: 61, to: 648 }, 79 | mark: MarkdownMark::Em, 80 | }) 81 | ); 82 | 83 | let s2: Step = serde_json::from_str( 84 | r#"{"stepType":"replace","from":986,"to":986,"slice":{"content":[{"type":"text","text":"!"}]}}"# 85 | ).unwrap(); 86 | 87 | assert_eq!( 88 | s2, 89 | Step::Replace(ReplaceStep { 90 | span: Span { from: 986, to: 986 }, 91 | slice: Slice { 92 | content: Fragment::from((MarkdownNode::text("!"),)), 93 | open_start: 0, 94 | open_end: 0, 95 | }, 96 | structure: false, 97 | }) 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/transform/replace_step.rs: -------------------------------------------------------------------------------- 1 | use super::{Span, StepError, StepKind, StepResult}; 2 | use crate::model::{Node, ResolveErr, Schema, Slice}; 3 | use derivative::Derivative; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// Replace some part of the document 7 | #[derive(Derivative, Deserialize, Serialize)] 8 | #[derivative(Debug(bound = ""), PartialEq(bound = ""), Eq(bound = ""))] 9 | #[serde(bound = "", rename_all = "camelCase")] 10 | pub struct ReplaceStep { 11 | /// The affected span 12 | #[serde(flatten)] 13 | pub span: Span, 14 | /// The slice to replace the current content with 15 | #[serde(default)] 16 | pub slice: Slice, 17 | /// Whether this is a structural change 18 | #[serde(default)] 19 | pub structure: bool, 20 | } 21 | 22 | impl StepKind for ReplaceStep { 23 | fn apply(&self, doc: &S::Node) -> StepResult { 24 | let from = self.span.from; 25 | let to = self.span.to; 26 | if self.structure && content_between::(doc, from, to)? { 27 | Err(StepError::WouldOverwrite) 28 | } else { 29 | let node = doc.replace(from..to, &self.slice)?; 30 | Ok(node) 31 | } 32 | } 33 | } 34 | 35 | /// Replace the document structure while keeping some content 36 | #[derive(Derivative, Deserialize, Serialize)] 37 | #[derivative(Debug(bound = ""), PartialEq(bound = ""), Eq(bound = ""))] 38 | #[serde(bound = "", rename_all = "camelCase")] 39 | pub struct ReplaceAroundStep { 40 | /// The affected part of the document 41 | #[serde(flatten)] 42 | pub span: Span, 43 | /// Start of the gap 44 | pub gap_from: usize, 45 | /// End of the gap 46 | pub gap_to: usize, 47 | /// The inner slice 48 | #[serde(default)] 49 | pub slice: Slice, 50 | /// ??? 51 | pub insert: usize, 52 | /// Whether this is a structural change 53 | #[serde(default)] 54 | pub structure: bool, 55 | } 56 | 57 | impl StepKind for ReplaceAroundStep { 58 | fn apply(&self, doc: &S::Node) -> StepResult { 59 | if self.structure 60 | && (content_between::(doc, self.span.from, self.gap_from)? 61 | || content_between::(doc, self.gap_to, self.span.to)?) 62 | { 63 | return Err(StepError::GapWouldOverwrite); 64 | } 65 | 66 | let gap = doc.slice(self.gap_from..self.gap_to, false)?; 67 | if gap.open_start != 0 || gap.open_end != 0 { 68 | return Err(StepError::GapNotFlat); 69 | } 70 | 71 | let inserted = self.slice.insert_at(self.insert, gap.content)?; 72 | let inserted = inserted.ok_or(StepError::GapNotFit)?; 73 | 74 | let result = doc.replace(self.span.from..self.span.to, &inserted)?; 75 | Ok(result) 76 | } 77 | } 78 | 79 | fn content_between(doc: &S::Node, from: usize, to: usize) -> Result { 80 | let rp_from = doc.resolve(from)?; 81 | let mut dist = to - from; 82 | let mut depth = rp_from.depth; 83 | while dist > 0 && depth > 0 && rp_from.index_after(depth) == rp_from.node(depth).child_count() { 84 | depth -= 1; 85 | dist -= 1; 86 | } 87 | if dist > 0 { 88 | let mut next = rp_from.node(depth).maybe_child(rp_from.index_after(depth)); 89 | while dist > 0 { 90 | match next { 91 | Some(c) => { 92 | if c.is_leaf() { 93 | return Ok(true); 94 | } else { 95 | next = c.first_child(); 96 | dist -= 1; 97 | } 98 | } 99 | None => { 100 | return Ok(true); 101 | } 102 | } 103 | } 104 | } 105 | Ok(false) 106 | } 107 | -------------------------------------------------------------------------------- /src/transform/step.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{InsertError, ReplaceError, ResolveErr, Schema, SliceError}; 2 | use derivative::Derivative; 3 | use displaydoc::Display; 4 | use thiserror::Error; 5 | 6 | /// Different ways a step application can fail 7 | #[derive(Derivative, Display, Error)] 8 | #[derivative(Debug(bound = ""))] 9 | pub enum StepError { 10 | /// Structure replace would overwrite content 11 | WouldOverwrite, 12 | /// Structure gap-replace would overwrite content 13 | GapWouldOverwrite, 14 | /// Gap is not a flat range 15 | GapNotFlat, 16 | /// Content does not fit in gap 17 | GapNotFit, 18 | /// Invalid indices 19 | Resolve(#[from] ResolveErr), 20 | /// Invalid resolve 21 | Replace(#[from] ReplaceError), 22 | /// Invalid slice 23 | Slice(#[from] SliceError), 24 | /// Insert error 25 | Insert(#[from] InsertError), 26 | } 27 | 28 | /// The result of [applying](#transform.Step.apply) a step. Contains either a 29 | /// new document or a failure value. 30 | #[allow(type_alias_bounds)] 31 | pub type StepResult = Result>; 32 | 33 | /// A step object represents an atomic change. 34 | /// 35 | /// It generally applies only to the document it was created for, since the positions 36 | /// stored in it will only make sense for that document. 37 | pub trait StepKind { 38 | /// Applies this step to the given document, returning a result 39 | /// object that either indicates failure, if the step can not be 40 | /// applied to this document, or indicates success by containing a 41 | /// transformed document. 42 | fn apply(&self, doc: &S::Node) -> StepResult; 43 | } 44 | -------------------------------------------------------------------------------- /src/transform/util.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// A span within a document 4 | #[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] 5 | pub struct Span { 6 | /// Start of the span 7 | pub from: usize, 8 | /// End of the span 9 | pub to: usize, 10 | } 11 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! # Generic utilities 2 | 3 | /// A type the holds a value of A or B or both. 4 | pub enum EitherOrBoth { 5 | /// Both values 6 | Both(A, B), 7 | /// Just a value of type A 8 | Left(A), 9 | /// Just a value of type B 10 | Right(B), 11 | } 12 | 13 | impl EitherOrBoth { 14 | /// Get the left value if present 15 | pub fn left(&self) -> Option<&A> { 16 | match self { 17 | Self::Both(a, _) => Some(a), 18 | Self::Left(a) => Some(a), 19 | Self::Right(_) => None, 20 | } 21 | } 22 | 23 | /// Get the right value if present 24 | pub fn right(&self) -> Option<&B> { 25 | match self { 26 | Self::Both(_, b) => Some(b), 27 | Self::Left(_) => None, 28 | Self::Right(b) => Some(b), 29 | } 30 | } 31 | } 32 | 33 | impl EitherOrBoth { 34 | /// Get the right value if present, and the left otherwise 35 | pub fn right_or_left(&self) -> &T { 36 | match self { 37 | Self::Left(a) => a, 38 | Self::Right(b) => b, 39 | Self::Both(_a, b) => b, 40 | } 41 | } 42 | } 43 | 44 | pub(crate) fn then_some(b: bool, v: T) -> Option { 45 | if b { 46 | Some(v) 47 | } else { 48 | None 49 | } 50 | } 51 | --------------------------------------------------------------------------------