1mod error;
2
3pub(crate) use twig_sys as ffi;
8
9use std::marker::PhantomData;
10use std::ops::Range;
11use std::os::raw::{c_char, c_int};
12use std::ptr::NonNull;
13
14pub use error::Error;
15pub use ffi::TwigSpan as Span;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum Format {
19 Djot,
20 Markdown,
21 Xml,
22 Html,
23}
24
25impl From<Format> for ffi::TwigFormat {
26 fn from(value: Format) -> Self {
27 match value {
28 Format::Djot => ffi::TwigFormat::Djot,
29 Format::Markdown => ffi::TwigFormat::Markdown,
30 Format::Xml => ffi::TwigFormat::Xml,
31 Format::Html => ffi::TwigFormat::Html,
32 }
33 }
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51#[non_exhaustive]
52pub enum Target {
53 Djot,
54 Markdown,
55 Xml,
56 Html,
57}
58
59impl Target {
60 pub fn as_format(self) -> Option<Format> {
67 match self {
68 Target::Djot => Some(Format::Djot),
69 Target::Markdown => Some(Format::Markdown),
70 Target::Xml => Some(Format::Xml),
71 Target::Html => Some(Format::Html),
72 }
73 }
74}
75
76impl From<Format> for Target {
80 fn from(value: Format) -> Self {
81 match value {
82 Format::Djot => Target::Djot,
83 Format::Markdown => Target::Markdown,
84 Format::Xml => Target::Xml,
85 Format::Html => Target::Html,
86 }
87 }
88}
89
90impl From<Target> for ffi::TwigFormat {
91 fn from(value: Target) -> Self {
92 match value {
93 Target::Djot => ffi::TwigFormat::Djot,
94 Target::Markdown => ffi::TwigFormat::Markdown,
95 Target::Xml => ffi::TwigFormat::Xml,
96 Target::Html => ffi::TwigFormat::Html,
97 }
98 }
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct QueryMatch {
104 pub node_id: u32,
106 pub span: Range<usize>,
108 pub content_span: Option<Range<usize>>,
111 pub kind: String,
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct Change {
123 pub old: Range<usize>,
124 pub new: Range<usize>,
125}
126
127impl Change {
128 pub fn delta(&self) -> isize {
130 self.new.len() as isize - self.old.len() as isize
131 }
132
133 fn from_ffi(c: ffi::TwigChange) -> Self {
134 Change {
135 old: c.old_span.start..c.old_span.end,
136 new: c.new_span.start..c.new_span.end,
137 }
138 }
139}
140
141#[derive(Clone, Debug, Eq, PartialEq)]
152#[non_exhaustive]
153pub struct FlatNode {
154 pub id: NodeId,
155 pub parent: Option<NodeId>,
156 pub first_child: Option<NodeId>,
157 pub next_sibling: Option<NodeId>,
158 pub span: Range<usize>,
159 pub content_span: Option<Range<usize>>,
160 pub level: Option<u32>,
162 pub kind: String,
163 pub text: Option<String>,
164 pub destination: Option<String>,
165 pub head: Option<bool>,
168 pub alignment: Option<Alignment>,
174 pub name: Option<String>,
186 pub directive_form: Option<DirectiveForm>,
193 pub attrs: Vec<(String, Option<String>)>,
197}
198
199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
204pub enum InlineKind {
205 Strong,
206 Emph,
207 Verbatim,
208 Mark,
209 Superscript,
210 Subscript,
211 Insert,
212 Delete,
213}
214
215impl InlineKind {
216 fn to_c(self) -> c_int {
217 match self {
218 InlineKind::Strong => 0,
219 InlineKind::Emph => 1,
220 InlineKind::Verbatim => 2,
221 InlineKind::Mark => 3,
222 InlineKind::Superscript => 4,
223 InlineKind::Subscript => 5,
224 InlineKind::Insert => 6,
225 InlineKind::Delete => 7,
226 }
227 }
228}
229
230#[derive(Clone, Copy, Debug, Eq, PartialEq)]
232pub enum BlockKind {
233 Paragraph,
234 Heading(u32),
236}
237
238impl BlockKind {
239 fn to_c(self) -> (c_int, u32) {
241 match self {
242 BlockKind::Paragraph => (0, 0),
243 BlockKind::Heading(level) => (1, level),
244 }
245 }
246}
247
248#[derive(Clone, Copy, Debug, Eq, PartialEq)]
254pub enum BlockContainerKind {
255 BlockQuote,
256 BulletList,
257 OrderedList,
258}
259
260impl BlockContainerKind {
261 fn to_c(self) -> c_int {
262 match self {
263 BlockContainerKind::BlockQuote => 0,
264 BlockContainerKind::BulletList => 1,
265 BlockContainerKind::OrderedList => 2,
266 }
267 }
268}
269
270#[derive(Clone, Copy, Debug, Eq, PartialEq)]
271pub struct Version {
272 pub major: u8,
273 pub minor: u8,
274 pub patch: u8,
275}
276
277pub fn version() -> Version {
278 let packed = unsafe { ffi::twig_version() };
279 Version {
280 major: (packed >> 16) as u8,
281 minor: (packed >> 8) as u8,
282 patch: packed as u8,
283 }
284}
285
286pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
292
293pub fn abi_version() -> u32 {
299 unsafe { ffi::twig_abi_version() }
300}
301
302pub fn version_string() -> &'static str {
303 let ptr = unsafe { ffi::twig_version_string() };
304 unsafe { std::ffi::CStr::from_ptr(ptr) }
305 .to_str()
306 .unwrap_or("")
307}
308
309#[derive(Debug)]
310pub struct Document {
311 raw: NonNull<ffi::TwigDocument>,
312}
313
314impl Document {
315 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
316 Self::parse_with(input, format, MarkdownExtensions::default())
317 }
318
319 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
320 Self::parse(input.as_bytes(), format)
321 }
322
323 pub fn parse_with(
329 input: &[u8],
330 format: Format,
331 extensions: MarkdownExtensions,
332 ) -> Result<Self, Error> {
333 let mut raw = std::ptr::null_mut();
334 let ffi_format: ffi::TwigFormat = format.into();
335 let status = unsafe {
336 ffi::twig_parse_ext(
337 input.as_ptr(),
338 input.len(),
339 ffi_format as i32,
340 extensions.to_flags(),
341 &mut raw,
342 )
343 };
344 Error::from_status(status)?;
345 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
346 Ok(Self { raw })
347 }
348
349 pub fn parse_str_with(
351 input: &str,
352 format: Format,
353 extensions: MarkdownExtensions,
354 ) -> Result<Self, Error> {
355 Self::parse_with(input.as_bytes(), format, extensions)
356 }
357
358 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
361 let raw = self.raw.as_ptr();
362 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
363 }
364
365 pub fn serialize_to(&mut self, target: Target) -> Result<Vec<u8>, Error> {
376 let raw = self.raw.as_ptr();
377 let ffi_target: ffi::TwigFormat = target.into();
378 collect_bytes(|ptr, len| unsafe {
379 ffi::twig_document_serialize(raw, ffi_target as i32, ptr, len)
380 })
381 }
382
383 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
390 self.serialize_to(format.into())
391 }
392
393 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
396 let raw = self.raw.as_ptr();
397 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
398 }
399
400 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
409 let raw = self.raw.as_ptr();
410 collect_matches(|ptr, len| unsafe {
411 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
412 })
413 }
414
415 pub fn span(&mut self, node: NodeId) -> Result<Range<usize>, Error> {
417 let mut span = ffi::TwigSpan { start: 0, end: 0 };
418 let status = unsafe { ffi::twig_document_node_span(self.raw.as_ptr(), node.0, &mut span) };
419 Error::from_status(status)?;
420 Ok(span.start..span.end)
421 }
422
423 pub fn content_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
426 let mut span = ffi::TwigSpan { start: 0, end: 0 };
427 let status = unsafe {
428 ffi::twig_document_node_content_span(self.raw.as_ptr(), node.0, &mut span)
429 };
430 match status.0 {
431 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
432 ffi::TwigStatus::NOT_FOUND => Ok(None),
433 _ => Err(Error::from_status(status).unwrap_err()),
434 }
435 }
436
437 pub fn cell_extent(&mut self, node: NodeId) -> Result<Option<(u32, u32)>, Error> {
449 let raw = self.raw.as_ptr();
450 let mut colspan: u32 = 0;
451 let status = unsafe { ffi::twig_document_cell_colspan(raw, node.0, &mut colspan) };
452 match status.0 {
453 ffi::TwigStatus::OK => {}
454 ffi::TwigStatus::NOT_FOUND => return Ok(None),
455 _ => return Err(Error::from_status(status).unwrap_err()),
456 }
457 let mut rowspan: u32 = 0;
458 Error::from_status(unsafe {
459 ffi::twig_document_cell_rowspan(raw, node.0, &mut rowspan)
460 })?;
461 Ok(Some((colspan, rowspan)))
462 }
463
464 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
469 let raw = self.raw.as_ptr();
470 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_nodes(raw, ptr, len) })
471 }
472
473 pub fn children(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
479 let raw = self.raw.as_ptr();
480 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
481 collect_matches(|ptr, len| unsafe { ffi::twig_document_children(raw, id, ptr, len) })
482 }
483
484 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
490 let raw = self.raw.as_ptr();
491 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_subtree(raw, node.0, ptr, len) })
492 }
493
494 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
499 let mut m = empty_ffi_match();
500 let status = unsafe { ffi::twig_document_node_at(self.raw.as_ptr(), offset, &mut m) };
501 match status.0 {
502 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
503 ffi::TwigStatus::NOT_FOUND => Ok(None),
504 _ => Err(Error::from_status(status).unwrap_err()),
505 }
506 }
507
508 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
512 let raw = self.raw.as_ptr();
513 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
514 let mut len = 0usize;
515 let status = unsafe { ffi::twig_document_nodes_at(raw, offset, &mut ptr, &mut len) };
516 match status.0 {
517 ffi::TwigStatus::OK => {}
518 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
519 _ => return Err(Error::from_status(status).unwrap_err()),
520 }
521 if len == 0 || ptr.is_null() {
522 return Ok(Vec::new());
523 }
524 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
525 raw_matches.iter().map(query_match_from_ffi).collect()
526 }
527}
528
529#[derive(Debug)]
540pub struct DocumentView<'a> {
541 doc: Document,
542 _editor: PhantomData<&'a mut Editor>,
543}
544
545impl std::ops::Deref for DocumentView<'_> {
546 type Target = Document;
547
548 fn deref(&self) -> &Document {
549 &self.doc
550 }
551}
552
553impl std::ops::DerefMut for DocumentView<'_> {
554 fn deref_mut(&mut self) -> &mut Document {
555 &mut self.doc
556 }
557}
558
559impl Drop for Document {
560 fn drop(&mut self) {
561 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
562 }
563}
564
565#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
571pub struct MarkdownExtensions {
572 pub directives: bool,
574 pub math: bool,
576 pub html_elements: bool,
581}
582
583impl MarkdownExtensions {
584 fn to_flags(self) -> u32 {
585 let mut flags = 0;
586 if self.directives {
587 flags |= ffi::TWIG_MD_DIRECTIVES;
588 }
589 if self.math {
590 flags |= ffi::TWIG_MD_MATH;
591 }
592 if self.html_elements {
593 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
594 }
595 flags
596 }
597}
598
599#[derive(Debug)]
605pub struct Editor {
606 raw: NonNull<ffi::TwigEditor>,
607}
608
609impl Editor {
610 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
613 let mut raw = std::ptr::null_mut();
614 let ffi_format: ffi::TwigFormat = format.into();
615 let status =
616 unsafe { ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
617 Error::from_status(status)?;
618 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
619 Ok(Self { raw })
620 }
621
622 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
623 Self::new(input.as_bytes(), format)
624 }
625
626 pub fn new_ext(input: &[u8], format: Format, extensions: MarkdownExtensions) -> Result<Self, Error> {
631 let mut raw = std::ptr::null_mut();
632 let ffi_format: ffi::TwigFormat = format.into();
633 let status = unsafe {
634 ffi::twig_editor_create_ext(
635 input.as_ptr(),
636 input.len(),
637 ffi_format as i32,
638 extensions.to_flags(),
639 &mut raw,
640 )
641 };
642 Error::from_status(status)?;
643 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
644 Ok(Self { raw })
645 }
646
647 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
649 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
650 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
651 })
652 }
653
654 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
657 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
658 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
659 })
660 }
661
662 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
664 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
665 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
666 })
667 }
668
669 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
671 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
672 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
673 })
674 }
675
676 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
679 let status = unsafe {
680 ffi::twig_editor_insert_child(
681 self.raw.as_ptr(),
682 locator.as_ptr(),
683 locator.len(),
684 index,
685 text.as_ptr(),
686 text.len(),
687 )
688 };
689 Error::from_status(status)
690 }
691
692 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
695 let status = unsafe {
696 ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len())
697 };
698 Error::from_status(status)
699 }
700
701 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
704 let status = unsafe {
705 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
706 };
707 Error::from_status(status)
708 }
709
710 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
714 let status = unsafe {
715 ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len())
716 };
717 Error::from_status(status)
718 }
719
720 pub fn filter(&mut self, drop: &str, keep: Option<&str>, unwrap_kept: bool) -> Result<(), Error> {
725 let (keep_ptr, keep_len) = match keep {
726 Some(k) => (k.as_ptr(), k.len()),
727 None => (std::ptr::null(), 0),
728 };
729 let status = unsafe {
730 ffi::twig_editor_filter(
731 self.raw.as_ptr(),
732 drop.as_ptr(),
733 drop.len(),
734 keep_ptr,
735 keep_len,
736 unwrap_kept as i32,
737 )
738 };
739 Error::from_status(status)
740 }
741
742 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
744 let raw = self.raw.as_ptr();
745 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
746 }
747
748 pub fn source_str(&mut self) -> Result<String, Error> {
750 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
751 }
752
753 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
756 let raw = self.raw.as_ptr();
757 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
758 }
759
760 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
763 let raw = self.raw.as_ptr();
764 collect_matches(|ptr, len| unsafe {
765 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
766 })
767 }
768
769 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
779 let mut change = ffi::TwigChange {
780 old_span: ffi::TwigSpan { start: 0, end: 0 },
781 new_span: ffi::TwigSpan { start: 0, end: 0 },
782 };
783 let status = unsafe {
784 ffi::twig_editor_edit_range(
785 self.raw.as_ptr(),
786 start,
787 end,
788 text.as_ptr(),
789 text.len(),
790 &mut change,
791 )
792 };
793 Error::from_status(status)?;
794 Ok(Change::from_ffi(change))
795 }
796
797 pub fn last_change(&mut self) -> Option<Change> {
803 let mut change = ffi::TwigChange {
804 old_span: ffi::TwigSpan { start: 0, end: 0 },
805 new_span: ffi::TwigSpan { start: 0, end: 0 },
806 };
807 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
808 match status.0 {
809 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
810 _ => None,
811 }
812 }
813
814 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
819 let mut change = ffi::TwigChange {
820 old_span: ffi::TwigSpan { start: 0, end: 0 },
821 new_span: ffi::TwigSpan { start: 0, end: 0 },
822 };
823 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
824 if status.0 == ffi::TwigStatus::NOT_FOUND {
825 return Ok(None);
826 }
827 Error::from_status(status)?;
828 Ok(Some(Change::from_ffi(change)))
829 }
830
831 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
835 let mut change = ffi::TwigChange {
836 old_span: ffi::TwigSpan { start: 0, end: 0 },
837 new_span: ffi::TwigSpan { start: 0, end: 0 },
838 };
839 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
840 if status.0 == ffi::TwigStatus::NOT_FOUND {
841 return Ok(None);
842 }
843 Error::from_status(status)?;
844 Ok(Some(Change::from_ffi(change)))
845 }
846
847 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
852 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
853 Error::from_status(status)
854 }
855
856 pub fn revision(&mut self) -> u64 {
862 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
863 }
864
865 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
886 let mut span = ffi::TwigSpan { start: 0, end: 0 };
887 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
888 match status.0 {
889 ffi::TwigStatus::OK => Some(span.start..span.end),
890 _ => None,
891 }
892 }
893
894 pub fn clear_dirty(&mut self) {
899 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
900 }
901
902 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
910 let status =
911 unsafe { ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len()) };
912 Error::from_status(status)
913 }
914
915 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
920 let raw = self.raw.as_ptr();
921 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
922 }
923
924 pub fn document(&mut self) -> Result<DocumentView<'_>, Error> {
933 let mut raw = std::ptr::null_mut();
934 let status = unsafe { ffi::twig_editor_document(self.raw.as_ptr(), &mut raw) };
935 Error::from_status(status)?;
936 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
937 Ok(DocumentView {
938 doc: Document { raw },
939 _editor: PhantomData,
940 })
941 }
942
943 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
948 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
949 let mut len = 0usize;
950 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
951 Error::from_status(status)?;
952 if len == 0 {
953 return Ok(Vec::new());
954 }
955 if ptr.is_null() {
956 return Err(Error::Internal);
957 }
958 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
959 raw.iter().map(flat_node_from_ffi).collect()
960 }
961
962 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
969 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
970 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
971 let mut len = 0usize;
972 let status =
973 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
974 Error::from_status(status)?;
975 if len == 0 || ptr.is_null() {
976 return Ok(Vec::new());
977 }
978 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
979 raw.iter().map(query_match_from_ffi).collect()
980 }
981
982 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
990 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
991 let mut len = 0usize;
992 let status =
993 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
994 Error::from_status(status)?;
995 if len == 0 || ptr.is_null() {
996 return Ok(Vec::new());
997 }
998 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
999 raw.iter().map(flat_node_from_ffi).collect()
1000 }
1001
1002 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1007 let mut m = ffi::TwigQueryMatch {
1008 node_id: 0,
1009 span: ffi::TwigSpan { start: 0, end: 0 },
1010 content_span: ffi::TwigSpan { start: 0, end: 0 },
1011 has_content_span: 0,
1012 kind: std::ptr::null(),
1013 };
1014 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
1015 match status.0 {
1016 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1017 ffi::TwigStatus::NOT_FOUND => Ok(None),
1018 _ => Err(Error::from_status(status).unwrap_err()),
1019 }
1020 }
1021
1022 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1026 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1027 let mut len = 0usize;
1028 let status =
1029 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
1030 match status.0 {
1031 ffi::TwigStatus::OK => {}
1032 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1033 _ => return Err(Error::from_status(status).unwrap_err()),
1034 }
1035 if len == 0 || ptr.is_null() {
1036 return Ok(Vec::new());
1037 }
1038 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1039 raw.iter().map(query_match_from_ffi).collect()
1040 }
1041
1042 pub fn wrap_range(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
1050 self.change_op(|ed, out| unsafe {
1051 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
1052 })
1053 }
1054
1055 pub fn toggle_inline(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
1060 self.change_op(|ed, out| unsafe {
1061 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
1062 })
1063 }
1064
1065 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
1072 let (block_kind, level) = kind.to_c();
1073 self.change_op(|ed, out| unsafe {
1074 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
1075 })
1076 }
1077
1078 pub fn toggle_block_container(
1101 &mut self,
1102 start: usize,
1103 end: usize,
1104 kind: BlockContainerKind,
1105 ) -> Result<Change, Error> {
1106 self.change_op(|ed, out| unsafe {
1107 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
1108 })
1109 }
1110
1111 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
1123 self.change_op(|ed, out| unsafe {
1124 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
1125 })?;
1126 Ok(())
1127 }
1128
1129 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
1138 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
1139 }
1140
1141 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
1144 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
1145 }
1146
1147 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
1149 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
1150 }
1151
1152 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
1154 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
1155 }
1156
1157 pub fn table_set_alignment(&mut self, offset: usize, alignment: Alignment) -> Result<(), Error> {
1159 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
1160 }
1161
1162 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
1164 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
1165 }
1166
1167 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
1169 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
1170 }
1171
1172 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
1173 self.change_op(|ed, out| unsafe {
1174 ffi::twig_editor_table_edit(ed, offset, op, arg, out)
1175 })?;
1176 Ok(())
1177 }
1178
1179 pub fn insert_link(
1224 &mut self,
1225 start: usize,
1226 end: usize,
1227 destination: &str,
1228 ) -> Result<Change, Error> {
1229 self.change_op(|ed, out| unsafe {
1230 ffi::twig_editor_insert_link(
1231 ed,
1232 start,
1233 end,
1234 destination.as_ptr(),
1235 destination.len(),
1236 out,
1237 )
1238 })
1239 }
1240
1241 pub fn insert_image(
1262 &mut self,
1263 start: usize,
1264 end: usize,
1265 destination: &str,
1266 ) -> Result<Change, Error> {
1267 self.change_op(|ed, out| unsafe {
1268 ffi::twig_editor_insert_image(
1269 ed,
1270 start,
1271 end,
1272 destination.as_ptr(),
1273 destination.len(),
1274 out,
1275 )
1276 })
1277 }
1278
1279 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
1300 self.change_op(|ed, out| unsafe {
1301 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
1302 })
1303 }
1304
1305 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
1319 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
1320 }
1321
1322 fn change_op(
1325 &mut self,
1326 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
1327 ) -> Result<Change, Error> {
1328 let mut change = ffi::TwigChange {
1329 old_span: ffi::TwigSpan { start: 0, end: 0 },
1330 new_span: ffi::TwigSpan { start: 0, end: 0 },
1331 };
1332 let status = op(self.raw.as_ptr(), &mut change);
1333 Error::from_status(status)?;
1334 Ok(Change::from_ffi(change))
1335 }
1336
1337 fn apply(
1339 &mut self,
1340 locator: &str,
1341 text: &str,
1342 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
1343 ) -> Result<(), Error> {
1344 let status = op(
1345 self.raw.as_ptr(),
1346 locator.as_ptr(),
1347 locator.len(),
1348 text.as_ptr(),
1349 text.len(),
1350 );
1351 Error::from_status(status)
1352 }
1353}
1354
1355impl Drop for Editor {
1356 fn drop(&mut self) {
1357 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
1358 }
1359}
1360
1361fn collect_bytes(
1366 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
1367) -> Result<Vec<u8>, Error> {
1368 let mut ptr = std::ptr::null();
1369 let mut len = 0usize;
1370 let status = call(&mut ptr, &mut len);
1371 Error::from_status(status)?;
1372 if len == 0 {
1373 return Ok(Vec::new());
1374 }
1375 if ptr.is_null() {
1376 return Err(Error::Internal);
1377 }
1378 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1379 Ok(bytes.to_vec())
1380}
1381
1382fn collect_matches(
1385 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
1386) -> Result<Vec<QueryMatch>, Error> {
1387 let mut ptr = std::ptr::null();
1388 let mut len = 0usize;
1389 let status = call(&mut ptr, &mut len);
1390 Error::from_status(status)?;
1391 if len == 0 {
1392 return Ok(Vec::new());
1393 }
1394 if ptr.is_null() {
1395 return Err(Error::Internal);
1396 }
1397 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1398 matches.iter().map(query_match_from_ffi).collect()
1399}
1400
1401fn collect_flat_nodes(
1404 call: impl FnOnce(*mut *const ffi::TwigFlatNode, *mut usize) -> ffi::TwigStatus,
1405) -> Result<Vec<FlatNode>, Error> {
1406 let mut ptr = std::ptr::null();
1407 let mut len = 0usize;
1408 let status = call(&mut ptr, &mut len);
1409 Error::from_status(status)?;
1410 if len == 0 {
1411 return Ok(Vec::new());
1412 }
1413 if ptr.is_null() {
1414 return Err(Error::Internal);
1415 }
1416 let nodes = unsafe { std::slice::from_raw_parts(ptr, len) };
1417 nodes.iter().map(flat_node_from_ffi).collect()
1418}
1419
1420fn empty_ffi_match() -> ffi::TwigQueryMatch {
1422 ffi::TwigQueryMatch {
1423 node_id: 0,
1424 span: ffi::TwigSpan { start: 0, end: 0 },
1425 content_span: ffi::TwigSpan { start: 0, end: 0 },
1426 has_content_span: 0,
1427 kind: std::ptr::null(),
1428 }
1429}
1430
1431fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
1434 Ok(QueryMatch {
1435 node_id: m.node_id,
1436 span: m.span.start..m.span.end,
1437 content_span: if m.has_content_span != 0 {
1438 Some(m.content_span.start..m.content_span.end)
1439 } else {
1440 None
1441 },
1442 kind: borrowed_cstr(m.kind)?,
1443 })
1444}
1445
1446fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
1448 let node_id = |v: u32| if v == ffi::TWIG_NO_NODE { None } else { Some(NodeId(v)) };
1449 Ok(FlatNode {
1450 id: NodeId(n.id),
1451 parent: node_id(n.parent),
1452 first_child: node_id(n.first_child),
1453 next_sibling: node_id(n.next_sibling),
1454 span: n.span.start..n.span.end,
1455 content_span: if n.has_content_span != 0 {
1456 Some(n.content_span.start..n.content_span.end)
1457 } else {
1458 None
1459 },
1460 level: if n.level != 0 { Some(n.level) } else { None },
1461 kind: borrowed_cstr(n.kind)?,
1462 text: borrowed_bytes(n.text_ptr, n.text_len),
1463 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
1464 head: match n.head {
1465 ffi::TWIG_HEAD_NONE => None,
1466 v => Some(v != 0),
1467 },
1468 alignment: Alignment::from_c(n.alignment),
1469 name: borrowed_bytes(n.name_ptr, n.name_len),
1470 directive_form: DirectiveForm::from_c(n.directive_form),
1471 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
1472 })
1473}
1474
1475fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
1479 if ptr.is_null() || len == 0 {
1480 return Vec::new();
1481 }
1482 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
1483 kvs.iter()
1484 .map(|kv| {
1485 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
1486 (key, borrowed_bytes(kv.value, kv.value_len))
1487 })
1488 .collect()
1489}
1490
1491fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
1493 if ptr.is_null() {
1494 return Err(Error::Internal);
1495 }
1496 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
1497 .to_str()
1498 .map_err(|_| Error::Internal)?
1499 .to_owned())
1500}
1501
1502fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
1506 if ptr.is_null() {
1507 return None;
1508 }
1509 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1510 Some(String::from_utf8_lossy(bytes).into_owned())
1511}
1512
1513#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1517pub struct NodeId(pub u32);
1518
1519#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1522pub enum VoidKind {
1523 Doc,
1524 Para,
1525 ThematicBreak,
1526 Section,
1527 Div,
1528 BlockQuote,
1529 DefinitionList,
1530 Table,
1531 ListItem,
1532 DefinitionListItem,
1533 Term,
1534 Definition,
1535 Caption,
1536 SoftBreak,
1537 HardBreak,
1538 NonBreakingSpace,
1539 Emph,
1540 Strong,
1541 Span,
1542 Mark,
1543 Superscript,
1544 Subscript,
1545 Insert,
1546 Delete,
1547 DoubleQuoted,
1548 SingleQuoted,
1549}
1550
1551impl VoidKind {
1552 fn to_c(self) -> c_int {
1553 match self {
1555 VoidKind::Doc => 0,
1556 VoidKind::Para => 1,
1557 VoidKind::ThematicBreak => 3,
1558 VoidKind::Section => 4,
1559 VoidKind::Div => 5,
1560 VoidKind::BlockQuote => 9,
1561 VoidKind::DefinitionList => 13,
1562 VoidKind::Table => 14,
1563 VoidKind::ListItem => 15,
1564 VoidKind::DefinitionListItem => 17,
1565 VoidKind::Term => 18,
1566 VoidKind::Definition => 19,
1567 VoidKind::Caption => 22,
1568 VoidKind::SoftBreak => 26,
1569 VoidKind::HardBreak => 27,
1570 VoidKind::NonBreakingSpace => 28,
1571 VoidKind::Emph => 38,
1572 VoidKind::Strong => 39,
1573 VoidKind::Span => 42,
1574 VoidKind::Mark => 43,
1575 VoidKind::Superscript => 44,
1576 VoidKind::Subscript => 45,
1577 VoidKind::Insert => 46,
1578 VoidKind::Delete => 47,
1579 VoidKind::DoubleQuoted => 48,
1580 VoidKind::SingleQuoted => 49,
1581 }
1582 }
1583}
1584
1585#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1587pub enum TextKind {
1588 Str,
1589 Symb,
1590 Verbatim,
1591 InlineMath,
1592 DisplayMath,
1593 Url,
1594 Email,
1595 FootnoteReference,
1596 CitationReference,
1599 SubstitutionReference,
1601 Comment,
1602 Doctype,
1603 Cdata,
1604}
1605
1606impl TextKind {
1607 fn to_c(self) -> c_int {
1608 match self {
1609 TextKind::Str => 25,
1610 TextKind::Symb => 29,
1611 TextKind::Verbatim => 30,
1612 TextKind::InlineMath => 32,
1613 TextKind::DisplayMath => 33,
1614 TextKind::Url => 34,
1615 TextKind::Email => 35,
1616 TextKind::FootnoteReference => 36,
1617 TextKind::CitationReference => 58,
1618 TextKind::SubstitutionReference => 59,
1619 TextKind::Comment => 52,
1620 TextKind::Doctype => 53,
1621 TextKind::Cdata => 55,
1622 }
1623 }
1624}
1625
1626#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1628pub enum BulletStyle {
1629 Dash,
1630 Plus,
1631 Star,
1632}
1633
1634impl BulletStyle {
1635 fn to_c(self) -> c_int {
1636 match self {
1637 BulletStyle::Dash => 0,
1638 BulletStyle::Plus => 1,
1639 BulletStyle::Star => 2,
1640 }
1641 }
1642}
1643
1644#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1646pub enum OrderedNumbering {
1647 Decimal,
1648 LowerAlpha,
1649 UpperAlpha,
1650 LowerRoman,
1651 UpperRoman,
1652}
1653
1654impl OrderedNumbering {
1655 fn to_c(self) -> c_int {
1656 match self {
1657 OrderedNumbering::Decimal => 0,
1658 OrderedNumbering::LowerAlpha => 1,
1659 OrderedNumbering::UpperAlpha => 2,
1660 OrderedNumbering::LowerRoman => 3,
1661 OrderedNumbering::UpperRoman => 4,
1662 }
1663 }
1664}
1665
1666#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1668pub enum OrderedDelim {
1669 Period,
1670 ParenAfter,
1671 ParenBoth,
1672}
1673
1674impl OrderedDelim {
1675 fn to_c(self) -> c_int {
1676 match self {
1677 OrderedDelim::Period => 0,
1678 OrderedDelim::ParenAfter => 1,
1679 OrderedDelim::ParenBoth => 2,
1680 }
1681 }
1682}
1683
1684#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1687pub enum Alignment {
1688 Default,
1689 Left,
1690 Right,
1691 Center,
1692}
1693
1694impl Alignment {
1695 fn to_c(self) -> c_int {
1696 match self {
1697 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
1698 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
1699 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
1700 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
1701 }
1702 }
1703
1704 fn from_c(v: c_int) -> Option<Self> {
1707 match v {
1708 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
1709 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
1710 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
1711 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
1712 _ => None,
1713 }
1714 }
1715}
1716
1717#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1719pub enum SmartPunctuation {
1720 LeftSingleQuote,
1721 RightSingleQuote,
1722 LeftDoubleQuote,
1723 RightDoubleQuote,
1724 Ellipses,
1725 EmDash,
1726 EnDash,
1727}
1728
1729impl SmartPunctuation {
1730 fn to_c(self) -> c_int {
1731 match self {
1732 SmartPunctuation::LeftSingleQuote => 0,
1733 SmartPunctuation::RightSingleQuote => 1,
1734 SmartPunctuation::LeftDoubleQuote => 2,
1735 SmartPunctuation::RightDoubleQuote => 3,
1736 SmartPunctuation::Ellipses => 4,
1737 SmartPunctuation::EmDash => 5,
1738 SmartPunctuation::EnDash => 6,
1739 }
1740 }
1741}
1742
1743#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1745pub enum DirectiveForm {
1746 Text,
1747 Leaf,
1748 Container,
1749}
1750
1751impl DirectiveForm {
1752 fn to_c(self) -> c_int {
1753 match self {
1754 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
1755 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
1756 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
1757 }
1758 }
1759
1760 fn from_c(v: c_int) -> Option<Self> {
1764 match v {
1765 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
1766 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
1767 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
1768 _ => None,
1769 }
1770 }
1771}
1772
1773fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
1777 match s {
1778 Some(x) => (x.as_ptr(), x.len(), 1),
1779 None => (std::ptr::null(), 0, 0),
1780 }
1781}
1782
1783#[derive(Debug)]
1790pub struct Builder {
1791 raw: NonNull<ffi::TwigBuilder>,
1792}
1793
1794impl Builder {
1795 pub fn new() -> Result<Self, Error> {
1797 let mut raw = std::ptr::null_mut();
1798 let status = unsafe { ffi::twig_builder_create(&mut raw) };
1799 Error::from_status(status)?;
1800 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1801 Ok(Self { raw })
1802 }
1803
1804 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
1807 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
1808 }
1809
1810 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
1812 self.emit(|b, out| unsafe { ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out) })
1813 }
1814
1815 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
1817 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
1818 }
1819
1820 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
1822 let (lp, ll, has) = opt_str(lang);
1823 self.emit(|b, out| unsafe { ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out) })
1824 }
1825
1826 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1828 self.emit(|b, out| unsafe {
1829 ffi::twig_builder_add_raw_block(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1830 })
1831 }
1832
1833 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
1835 self.emit(|b, out| unsafe {
1836 ffi::twig_builder_add_metadata(b, lang.as_ptr(), lang.len(), text.as_ptr(), text.len(), out)
1837 })
1838 }
1839
1840 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1842 self.emit(|b, out| unsafe {
1843 ffi::twig_builder_add_raw_inline(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1844 })
1845 }
1846
1847 pub fn add_smart_punctuation(&mut self, kind: SmartPunctuation, text: &str) -> Result<NodeId, Error> {
1852 self.emit(|b, out| unsafe {
1853 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
1854 })
1855 }
1856
1857 pub fn add_link(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1860 let (dp, dl, hd) = opt_str(destination);
1861 let (rp, rl, hr) = opt_str(reference);
1862 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
1863 }
1864
1865 pub fn add_image(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1867 let (dp, dl, hd) = opt_str(destination);
1868 let (rp, rl, hr) = opt_str(reference);
1869 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
1870 }
1871
1872 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
1874 self.emit(|b, out| unsafe { ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out) })
1875 }
1876
1877 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
1879 self.emit(|b, out| unsafe { ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out) })
1880 }
1881
1882 pub fn add_processing_instruction(&mut self, target: &str, data: &str) -> Result<NodeId, Error> {
1884 self.emit(|b, out| unsafe {
1885 ffi::twig_builder_add_processing_instruction(b, target.as_ptr(), target.len(), data.as_ptr(), data.len(), out)
1886 })
1887 }
1888
1889 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
1891 self.emit(|b, out| unsafe { ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out) })
1892 }
1893
1894 pub fn add_citation(&mut self, label: &str) -> Result<NodeId, Error> {
1899 self.emit(|b, out| unsafe { ffi::twig_builder_add_citation(b, label.as_ptr(), label.len(), out) })
1900 }
1901
1902 pub fn add_substitution(&mut self, label: &str) -> Result<NodeId, Error> {
1906 self.emit(|b, out| unsafe { ffi::twig_builder_add_substitution(b, label.as_ptr(), label.len(), out) })
1907 }
1908
1909 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
1911 self.emit(|b, out| unsafe {
1912 ffi::twig_builder_add_reference(b, label.as_ptr(), label.len(), destination.as_ptr(), destination.len(), out)
1913 })
1914 }
1915
1916 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
1918 self.emit(|b, out| unsafe { ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out) })
1919 }
1920
1921 pub fn add_ordered_list(
1923 &mut self,
1924 numbering: OrderedNumbering,
1925 delim: OrderedDelim,
1926 tight: bool,
1927 start: Option<u32>,
1928 ) -> Result<NodeId, Error> {
1929 let (start_val, has_start) = match start {
1930 Some(s) => (s, 1),
1931 None => (0, 0),
1932 };
1933 self.emit(|b, out| unsafe {
1934 ffi::twig_builder_add_ordered_list(b, numbering.to_c(), delim.to_c(), tight as c_int, start_val, has_start, out)
1935 })
1936 }
1937
1938 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
1940 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
1941 }
1942
1943 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
1945 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list_item(b, checked as c_int, out) })
1946 }
1947
1948 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
1950 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
1951 }
1952
1953 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
1955 self.emit(|b, out| unsafe { ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out) })
1956 }
1957
1958 pub fn add_cell_spanning(
1963 &mut self,
1964 head: bool,
1965 alignment: Alignment,
1966 colspan: u32,
1967 rowspan: u32,
1968 ) -> Result<NodeId, Error> {
1969 self.emit(|b, out| unsafe {
1970 ffi::twig_builder_add_cell_spanning(b, head as c_int, alignment.to_c(), colspan, rowspan, out)
1971 })
1972 }
1973
1974 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
1977 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
1978 let status = unsafe { ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len()) };
1979 Error::from_status(status)
1980 }
1981
1982 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
1986 let kvs: Vec<ffi::TwigKeyVal> = attrs
1987 .iter()
1988 .map(|(k, v)| ffi::TwigKeyVal {
1989 key: k.as_ptr(),
1990 key_len: k.len(),
1991 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
1992 value_len: v.map_or(0, |s| s.len()),
1993 })
1994 .collect();
1995 let status = unsafe { ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len()) };
1996 Error::from_status(status)
1997 }
1998
1999 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
2002 let raw = self.raw.as_ptr();
2003 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
2004 }
2005
2006 pub fn serialize_to(&mut self, root: NodeId, target: Target) -> Result<Vec<u8>, Error> {
2013 let raw = self.raw.as_ptr();
2014 let ffi_target: ffi::TwigFormat = target.into();
2015 collect_bytes(|ptr, len| unsafe {
2016 ffi::twig_builder_serialize(raw, root.0, ffi_target as i32, ptr, len)
2017 })
2018 }
2019
2020 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
2025 self.serialize_to(root, format.into())
2026 }
2027
2028 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
2030 let raw = self.raw.as_ptr();
2031 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
2032 }
2033
2034 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
2037 let raw = self.raw.as_ptr();
2038 collect_matches(|ptr, len| unsafe {
2039 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
2040 })
2041 }
2042
2043 fn emit(
2046 &mut self,
2047 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
2048 ) -> Result<NodeId, Error> {
2049 let mut id: u32 = 0;
2050 let status = call(self.raw.as_ptr(), &mut id);
2051 Error::from_status(status)?;
2052 Ok(NodeId(id))
2053 }
2054}
2055
2056impl Drop for Builder {
2057 fn drop(&mut self) {
2058 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
2059 }
2060}
2061
2062#[cfg(test)]
2063mod tests {
2064 use super::*;
2065
2066 #[test]
2067 fn abi_version_matches() {
2068 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
2072 }
2073
2074 #[test]
2075 fn parses_and_renders_markdown_html() {
2076 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
2077 let html = doc.render_html().expect("render html");
2078 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
2079 }
2080
2081 #[test]
2082 fn parses_html_input() {
2083 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
2084 let html = doc.render_html().expect("render html");
2085 assert!(String::from_utf8_lossy(&html).contains("hi"));
2086 }
2087
2088 #[test]
2089 fn serialize_round_trips_and_cross_converts() {
2090 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
2091
2092 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
2093 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
2094
2095 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
2097 }
2098
2099 #[test]
2100 fn serialize_markdown_to_djot() {
2101 let mut doc =
2102 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
2103 let djot = doc.serialize(Format::Djot).expect("serialize djot");
2104 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
2105 }
2106
2107 #[test]
2108 fn serialize_to_takes_the_output_axis() {
2109 let mut doc =
2110 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
2111
2112 let djot = doc.serialize_to(Target::Djot).expect("serialize djot");
2113 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
2114
2115 assert_eq!(doc.serialize_to(Target::Xml), Err(Error::UnsupportedFormat));
2118 }
2119
2120 #[test]
2121 fn serialize_and_serialize_to_agree() {
2122 let mut a = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
2125 let mut b = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
2126 for format in [Format::Markdown, Format::Djot, Format::Html] {
2127 assert_eq!(a.serialize(format), b.serialize_to(Target::from(format)));
2128 }
2129 }
2130
2131 #[test]
2132 fn every_format_is_a_target_that_names_it_back() {
2133 for format in [Format::Djot, Format::Markdown, Format::Xml, Format::Html] {
2136 assert_eq!(Target::from(format).as_format(), Some(format));
2137 }
2138 }
2139
2140 #[test]
2141 fn ast_json_dumps_the_tree() {
2142 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
2143 let json = doc.ast_json().expect("ast json");
2144 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
2145 }
2146
2147 #[test]
2148 fn query_finds_nodes_by_selector() {
2149 let source = "# One\n\n## Two\n";
2150 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
2151 let matches = doc.query("heading").expect("query");
2152
2153 assert_eq!(matches.len(), 2);
2154 for m in &matches {
2155 assert_eq!(m.kind, "heading");
2156 assert!(m.span.start < m.span.end);
2157 }
2158 }
2159
2160 #[test]
2161 fn query_recovers_code_spans() {
2162 let source = "prose `code` more prose\n";
2163 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
2164 let matches = doc.query("verbatim").expect("query");
2165
2166 assert_eq!(matches.len(), 1);
2167 assert_eq!(&source[matches[0].span.clone()], "`code`");
2168 }
2169
2170 #[test]
2171 fn document_span_accessors_read_by_node_id() {
2172 let source = "# hi\n\ntext\n";
2173 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
2174 let heading = doc.query("heading").expect("query").pop().expect("heading");
2175
2176 assert_eq!(doc.span(NodeId(heading.node_id)).expect("span"), heading.span);
2177 assert_eq!(
2178 doc.content_span(NodeId(heading.node_id)).expect("content span"),
2179 heading.content_span
2180 );
2181 assert_eq!(doc.span(NodeId(u32::MAX)), Err(Error::InvalidArgument));
2182 }
2183
2184 #[test]
2185 fn document_walks_its_tree_without_an_editor() {
2186 let source = "# hi\n\ntext\n";
2187 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
2188
2189 let nodes = doc.nodes().expect("nodes");
2190 assert!(nodes.len() >= 3);
2191 for (i, n) in nodes.iter().enumerate() {
2192 assert_eq!(n.id, NodeId(i as u32));
2193 }
2194
2195 let kids = doc.children(None).expect("children");
2196 assert_eq!(kids.len(), 2);
2197 assert_eq!(kids[0].kind, "heading");
2198
2199 let sub = doc.subtree(NodeId(kids[0].node_id)).expect("subtree");
2200 assert_eq!(sub[0].id, NodeId(0));
2201 assert_eq!(sub[0].parent, None);
2202 assert_eq!(sub[0].span, kids[0].span);
2203
2204 let hit = doc.node_at(2).expect("node_at").expect("a node at 2");
2205 let chain = doc.ancestors_at(2).expect("ancestors");
2206 assert_eq!(chain.last().expect("deepest").node_id, hit.node_id);
2207 assert_eq!(chain[0].kind, "doc");
2208
2209 assert_eq!(doc.subtree(NodeId(u32::MAX)), Err(Error::InvalidArgument));
2210 }
2211
2212 #[test]
2213 fn editor_document_view_reads_the_live_tree() {
2214 let mut ed = Editor::new_str("# one\n\ntwo\n", Format::Markdown).expect("editor");
2215
2216 {
2217 let mut view = ed.document().expect("view");
2218 let kids = view.children(None).expect("children");
2219 assert_eq!(kids.len(), 2);
2220 assert_eq!(kids[0].kind, "heading");
2221 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..5);
2222 assert_eq!(view.render_html(), Err(Error::UnsupportedFormat));
2224 assert_eq!(view.serialize(Format::Markdown), Err(Error::UnsupportedFormat));
2225 }
2226
2227 ed.replace("0", "# one and a half").expect("replace");
2228 let mut view = ed.document().expect("view");
2229 let kids = view.children(None).expect("children");
2230 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..16);
2231 }
2232
2233 #[test]
2234 fn query_rejects_a_malformed_selector() {
2235 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
2236 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
2237 }
2238
2239 #[test]
2240 fn editor_edits_by_index_path() {
2241 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
2242 ed.replace_content("0.0", "bye").expect("replace_content");
2243 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
2244 }
2245
2246 #[test]
2247 fn flat_nodes_expose_element_name_and_attrs() {
2248 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
2252 let mut ed = Editor::new_ext(
2253 src.as_bytes(),
2254 Format::Markdown,
2255 MarkdownExtensions { html_elements: true, ..Default::default() },
2256 )
2257 .expect("editor");
2258 let nodes = ed.nodes().expect("nodes");
2259
2260 let source = nodes
2261 .iter()
2262 .find(|n| n.name.as_deref() == Some("source"))
2263 .expect("a <source> element node");
2264 assert_eq!(
2265 source.attrs,
2266 vec![
2267 ("media".to_string(), Some("(prefers-color-scheme: dark)".to_string())),
2268 ("srcset".to_string(), Some("d.svg".to_string())),
2269 ]
2270 );
2271
2272 let img = nodes.iter().find(|n| n.kind == "image").expect("an image node");
2275 assert!(img.name.is_none());
2276 assert_eq!(img.destination.as_deref(), Some("l.svg"));
2277
2278 let picture_kids_str = nodes.iter().find(|n| n.kind == "str");
2280 if let Some(s) = picture_kids_str {
2281 assert!(s.name.is_none() && s.attrs.is_empty());
2282 }
2283 }
2284
2285 #[test]
2286 fn flat_nodes_expose_directive_name_and_form() {
2287 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
2293 let mut ed = Editor::new_ext(
2294 src.as_bytes(),
2295 Format::Markdown,
2296 MarkdownExtensions { directives: true, ..Default::default() },
2297 )
2298 .expect("editor");
2299 let nodes = ed.nodes().expect("nodes");
2300
2301 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
2302 .iter()
2303 .filter(|n| n.kind == "container")
2304 .map(|n| (n.name.as_deref(), n.directive_form))
2305 .collect();
2306 assert_eq!(
2307 forms,
2308 vec![
2309 (Some("note"), Some(DirectiveForm::Container)),
2310 (Some("embed"), Some(DirectiveForm::Leaf)),
2311 (Some("abbr"), Some(DirectiveForm::Text)),
2312 ]
2313 );
2314
2315 let embed = nodes.iter().find(|n| n.name.as_deref() == Some("embed")).expect("embed");
2318 assert_eq!(embed.attrs, vec![("src".to_string(), Some("demo.html".to_string()))]);
2319 let para = nodes.iter().find(|n| n.kind == "para").expect("a para");
2320 assert!(para.directive_form.is_none() && para.name.is_none());
2321 }
2322
2323 #[test]
2324 fn editor_insert_child_and_delete() {
2325 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
2326 ed.insert_child("0", 1, "<b/>").expect("insert_child");
2327 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
2328 ed.delete("0.1").expect("delete");
2329 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
2330 }
2331
2332 #[test]
2333 fn editor_edits_by_selector() {
2334 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
2335 ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
2336 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
2337 }
2338
2339 #[test]
2340 fn editor_locator_errors_are_distinct() {
2341 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
2342 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
2343 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
2344 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
2345 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
2347 }
2348
2349 #[test]
2350 fn editor_reparse_break_rolls_back() {
2351 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
2352 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
2353 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
2354 }
2355
2356 #[test]
2357 fn editor_leaf_content_is_not_editable() {
2358 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2359 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
2360 }
2361
2362 #[test]
2363 fn editor_query_reflects_current_tree() {
2364 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
2365 ed.insert_child("0", 1, "<b/>").expect("insert_child");
2366 assert_eq!(ed.query("element").expect("query").len(), 3);
2368 let json = ed.ast_json().expect("ast_json");
2369 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
2370 }
2371
2372 #[test]
2375 fn editor_edit_range_types_backspaces_and_reports_change() {
2376 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
2377
2378 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
2380 assert_eq!(ed.source_str().unwrap(), "aXb\n");
2381 assert_eq!(c.old, 1..1);
2382 assert_eq!(c.new, 1..2);
2383 assert_eq!(c.delta(), 1);
2384
2385 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
2387 assert_eq!(ed.source_str().unwrap(), "ab\n");
2388 assert_eq!(c2.old, 1..2);
2389 assert_eq!(c2.new, 1..1);
2390 assert_eq!(c2.delta(), -1);
2391 }
2392
2393 #[test]
2394 fn editor_edit_range_rejects_bad_ranges() {
2395 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2396 assert_eq!(ed.edit_range(0, 99, "x"), Err(Error::InvalidArgument)); assert_eq!(ed.edit_range(2, 1, "x"), Err(Error::InvalidArgument)); assert_eq!(ed.source_str().unwrap(), "hi\n"); }
2400
2401 #[test]
2402 fn editor_last_change_reports_locator_ops_too() {
2403 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
2404 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
2407 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
2408 let c = ed.last_change().expect("a change was recorded");
2409 assert_eq!(c.old, 7..13);
2411 assert_eq!(c.new, 7..17);
2412 }
2413
2414 #[test]
2415 fn editor_nodes_is_a_walkable_flat_tree() {
2416 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
2417 let nodes = ed.nodes().expect("nodes");
2418 assert!(!nodes.is_empty());
2419
2420 for (i, n) in nodes.iter().enumerate() {
2422 assert_eq!(n.id, NodeId(i as u32));
2423 }
2424 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
2426 assert_eq!(roots.len(), 1);
2427 assert_eq!(roots[0].kind, "doc");
2428
2429 let heading = nodes.iter().find(|n| n.kind == "heading").expect("a heading");
2431 assert_eq!(heading.level, Some(1));
2432 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
2433
2434 assert_eq!(heading.head, None);
2436 assert_eq!(heading.alignment, None);
2437
2438 for n in nodes.iter().filter(|n| n.parent.is_some()) {
2441 let p = &nodes[n.parent.unwrap().0 as usize];
2442 let mut kid = p.first_child;
2443 let mut seen = false;
2444 while let Some(NodeId(k)) = kid {
2445 if k == n.id.0 {
2446 seen = true;
2447 break;
2448 }
2449 kid = nodes[k as usize].next_sibling;
2450 }
2451 assert!(seen, "node {:?} not found among its parent's children", n.id);
2452 }
2453 }
2454
2455 #[test]
2456 fn editor_child_spans_and_subtree_agree_with_nodes() {
2457 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
2458 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
2459 let all = ed.nodes().expect("nodes");
2460 let doc = all.iter().find(|n| n.kind == "doc").expect("doc");
2461
2462 let top = ed.child_spans(None).expect("child_spans");
2465 let mut want = Vec::new();
2466 let mut c = doc.first_child;
2467 while let Some(id) = c {
2468 want.push(id);
2469 c = all[id.0 as usize].next_sibling;
2470 }
2471 assert_eq!(top.len(), want.len(), "top-level count");
2472 for (m, id) in top.iter().zip(&want) {
2473 assert_eq!(m.node_id, id.0, "child id");
2474 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
2475 assert_eq!(m.span, all[id.0 as usize].span, "child span");
2476 }
2477 assert!(src[top[0].span.clone()].starts_with('#'), "first block is the heading");
2479
2480 let list = top.iter().find(|m| m.kind.ends_with("list")).expect("a list");
2482 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
2483 assert_eq!(items.len(), 2);
2484 assert!(items.iter().all(|m| m.kind == "list_item"), "items: {items:?}");
2485
2486 let para = top.iter().find(|m| m.kind == "para").expect("a para").node_id;
2488 let sub = ed.subtree(NodeId(para)).expect("subtree");
2489 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
2490 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
2491 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
2492 assert_eq!(sub[0].kind, "para");
2493 for (i, n) in sub.iter().enumerate() {
2494 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
2495 for link in [n.parent, n.first_child, n.next_sibling].into_iter().flatten() {
2496 assert!((link.0 as usize) < sub.len(), "link {link:?} escapes the subtree");
2497 }
2498 }
2499 assert!(
2500 src[sub[0].span.clone()].starts_with("Hello"),
2501 "absolute span: {:?}",
2502 &src[sub[0].span.clone()]
2503 );
2504
2505 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<String> {
2507 let mut out = Vec::new();
2508 let mut stack = vec![root];
2509 while let Some(id) = stack.pop() {
2510 let n = &all[id.0 as usize];
2511 out.push(n.kind.clone());
2512 let mut c = n.first_child;
2513 while let Some(cid) = c {
2514 stack.push(cid);
2515 c = all[cid.0 as usize].next_sibling;
2516 }
2517 }
2518 out
2519 }
2520 let mut want_kinds = arena_kinds(&all, NodeId(para));
2521 let mut got_kinds: Vec<String> = sub.iter().map(|n| n.kind.clone()).collect();
2522 want_kinds.sort();
2523 got_kinds.sort();
2524 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
2525
2526 assert!(matches!(ed.subtree(NodeId(9999)), Err(Error::InvalidArgument)));
2528 }
2529
2530 #[test]
2531 fn flat_nodes_carry_table_head_and_alignment() {
2532 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
2536 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
2537 let nodes = ed.nodes().expect("nodes");
2538
2539 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == "row").collect();
2540 assert_eq!(rows.len(), 2, "a header row and one body row");
2541 assert_eq!(rows[0].head, Some(true), "first row is the header");
2542 assert_eq!(rows[1].head, Some(false), "second row is a body row");
2543
2544 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == "cell").collect();
2545 assert_eq!(cells.len(), 4);
2546 assert_eq!(cells[0].alignment, Some(Alignment::Left));
2548 assert_eq!(cells[1].alignment, Some(Alignment::Right));
2549 assert_eq!(cells[2].alignment, Some(Alignment::Left));
2550 assert_eq!(cells[3].alignment, Some(Alignment::Right));
2551 assert_eq!(cells[0].head, Some(true));
2553 assert_eq!(cells[2].head, Some(false));
2554
2555 let mut plain = Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
2558 let pnodes = plain.nodes().expect("nodes");
2559 let pcell = pnodes.iter().find(|n| n.kind == "cell").expect("a cell");
2560 assert_eq!(pcell.alignment, Some(Alignment::Default));
2561 }
2562
2563 #[test]
2564 fn cell_extent_reports_merged_cells_and_nothing_else() {
2565 let src = "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td><td>b</td></tr></table>";
2566 let mut doc = Document::parse_str(src, Format::Html).expect("parse");
2567 let cells: Vec<NodeId> = doc
2568 .nodes()
2569 .expect("nodes")
2570 .iter()
2571 .filter(|n| n.kind == "cell")
2572 .map(|n| n.id)
2573 .collect();
2574 assert_eq!(cells.len(), 2);
2575 assert_eq!(doc.cell_extent(cells[0]).expect("extent"), Some((2, 3)));
2576 assert_eq!(doc.cell_extent(cells[1]).expect("extent"), Some((1, 1)));
2578
2579 let mut pipe = Document::parse_str("| a |\n| --- |\n| b |\n", Format::Markdown).expect("parse");
2581 let pipe_cell = pipe
2582 .nodes()
2583 .expect("nodes")
2584 .iter()
2585 .find(|n| n.kind == "cell")
2586 .expect("a cell")
2587 .id;
2588 assert_eq!(pipe.cell_extent(pipe_cell).expect("extent"), Some((1, 1)));
2589
2590 let root = NodeId(0);
2592 assert_eq!(pipe.cell_extent(root).expect("extent"), None);
2593 }
2594
2595 #[test]
2596 fn builder_add_cell_spanning_renders_colspan_and_rowspan() {
2597 let mut b = Builder::new().expect("builder");
2598 let wide_text = b.add_text(TextKind::Str, "wide").expect("str");
2599 let wide = b.add_cell_spanning(false, Alignment::Default, 2, 3).expect("cell");
2600 b.set_children(wide, &[wide_text]).expect("children");
2601 let plain_text = b.add_text(TextKind::Str, "one").expect("str");
2602 let plain = b.add_cell(false, Alignment::Default).expect("cell");
2603 b.set_children(plain, &[plain_text]).expect("children");
2604 let row = b.add_row(false).expect("row");
2605 b.set_children(row, &[wide, plain]).expect("children");
2606 let table = b.add(VoidKind::Table).expect("table");
2607 b.set_children(table, &[row]).expect("children");
2608
2609 let html = String::from_utf8(b.render_html(table).expect("html")).expect("utf-8");
2610 assert!(html.contains("<td colspan=\"2\" rowspan=\"3\">wide</td>"), "{html}");
2611 assert!(html.contains("<td>one</td>"), "{html}");
2613
2614 assert!(matches!(
2616 b.add_cell_spanning(false, Alignment::Default, 0, 1),
2617 Err(Error::InvalidArgument)
2618 ));
2619 }
2620
2621 #[test]
2622 fn editor_node_at_and_ancestors_hit_test_offsets() {
2623 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
2624
2625 let m = ed.node_at(2).expect("node_at").expect("a node covers offset 2");
2627 assert!(m.span.contains(&2));
2628
2629 let chain = ed.ancestors_at(2).expect("ancestors_at");
2631 assert!(!chain.is_empty());
2632 assert_eq!(chain[0].kind, "doc");
2633 assert_eq!(chain.last().unwrap().node_id, m.node_id);
2634
2635 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
2637 }
2638
2639 #[test]
2642 fn editor_wrap_and_toggle_inline_round_trip() {
2643 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
2644
2645 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
2647 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
2648 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
2649
2650 ed.toggle_inline(4, 8, InlineKind::Strong).expect("toggle off");
2652 assert_eq!(ed.source_str().unwrap(), "a word b\n");
2653
2654 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
2656 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
2657 }
2658
2659 #[test]
2660 fn editor_inline_kind_support_is_format_specific() {
2661 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
2663 assert_eq!(md.wrap_range(2, 6, InlineKind::Mark), Err(Error::UnsupportedFormat));
2664
2665 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
2667 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
2668 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
2669 }
2670
2671 #[test]
2672 fn editor_toggle_strips_verbatim_via_content_span() {
2673 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
2674 ed.toggle_inline(2, 8, InlineKind::Verbatim).expect("toggle code off");
2676 assert_eq!(ed.source_str().unwrap(), "a code b\n");
2677
2678 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
2681 ed2.toggle_inline(2, 7, InlineKind::Verbatim).expect("toggle multi off");
2682 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
2683 }
2684
2685 #[test]
2686 fn editor_set_block_switches_para_and_heading_levels() {
2687 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
2688
2689 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
2691 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
2692
2693 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
2695 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
2696
2697 ed.set_block(2, BlockKind::Paragraph).expect("to para");
2699 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
2700 }
2701
2702 #[test]
2703 fn editor_set_block_rejects_bad_level_and_format() {
2704 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2705 assert_eq!(md.set_block(0, BlockKind::Heading(9)), Err(Error::InvalidArgument));
2706
2707 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2708 assert_eq!(xml.set_block(1, BlockKind::Heading(1)), Err(Error::UnsupportedFormat));
2709 }
2710
2711 #[test]
2712 fn editor_toggle_block_container_round_trips() {
2713 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
2714
2715 let c = ed
2716 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
2717 .expect("quote on");
2718 assert_eq!(ed.source_str().unwrap(), "> a\n");
2719 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
2720
2721 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2722 .expect("quote off");
2723 assert_eq!(ed.source_str().unwrap(), "a\n");
2724 }
2725
2726 #[test]
2727 fn editor_toggle_block_container_nests_a_partial_selection() {
2728 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
2729
2730 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2733 .expect("nest");
2734 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
2735
2736 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
2738 .expect("peel");
2739 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
2740 }
2741
2742 #[test]
2743 fn editor_toggle_block_container_numbers_and_converts_lists() {
2744 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
2745
2746 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
2748 .expect("ordered on");
2749 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
2750
2751 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
2753 .expect("convert");
2754 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
2755 }
2756
2757 #[test]
2758 fn editor_toggle_block_container_rejects_unspellable_format() {
2759 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2760 assert_eq!(
2761 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
2762 Err(Error::UnsupportedFormat)
2763 );
2764 }
2765
2766 #[test]
2767 fn editor_insert_link_wraps_and_repoints() {
2768 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
2769
2770 ed.insert_link(2, 6, "http://x.dev").expect("link");
2771 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
2772
2773 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
2775 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
2776 }
2777
2778 #[test]
2779 fn editor_insert_link_repoints_an_autolink() {
2780 for format in [Format::Markdown, Format::Djot] {
2785 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
2786 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
2787 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
2788
2789 let nodes = ed.nodes().expect("nodes");
2791 let url = nodes
2792 .iter()
2793 .find(|n| n.kind == "url")
2794 .expect("still an autolink");
2795 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
2796 assert!(!nodes.iter().any(|n| n.kind == "link"));
2797 }
2798 }
2799
2800 #[test]
2801 fn editor_insert_link_escapes_the_destination() {
2802 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
2805 dj.insert_link(0, 1, "a)b").expect("link");
2806 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
2807
2808 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
2812 md.insert_link(0, 1, "a b").expect("link");
2813 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
2814
2815 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
2816 dj2.insert_link(0, 1, "a b").expect("link");
2817 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
2818 }
2819
2820 #[test]
2821 fn editor_insert_image_escapes_the_destination_per_format() {
2822 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
2825 md.insert_image(0, 1, "my cat.png").expect("image");
2826 assert_eq!(md.source_str().unwrap(), "\n");
2827
2828 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
2829 dj.insert_image(0, 1, "my cat.png").expect("image");
2830 assert_eq!(dj.source_str().unwrap(), "\n");
2831
2832 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
2834 paren.insert_image(0, 1, "a)b.png").expect("image");
2835 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
2836 }
2837
2838 #[test]
2839 fn editor_insert_image_keeps_an_empty_alt_empty() {
2840 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
2843 ed.insert_image(1, 1, "cat.png").expect("image");
2844 assert_eq!(ed.source_str().unwrap(), "ab\n");
2845 }
2846
2847 #[test]
2848 fn editor_insert_image_rejects_a_newline_destination() {
2849 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
2850 assert_eq!(ed.insert_image(0, 1, "a\nb.png"), Err(Error::InvalidArgument));
2851
2852 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2853 assert_eq!(xml.insert_image(3, 5, "x.png"), Err(Error::UnsupportedFormat));
2854 }
2855
2856 #[test]
2857 fn editor_insert_link_rejects_a_newline_destination() {
2858 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
2859 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
2860
2861 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2862 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
2863 }
2864
2865 #[test]
2866 fn editor_insert_literal_keeps_typed_specials_literal() {
2867 for format in [Format::Markdown, Format::Djot] {
2868 let mut ed = Editor::new_str("z\n", format).expect("editor");
2869 ed.insert_literal(0, "*hi*").expect("literal");
2871
2872 let nodes = ed.nodes().expect("nodes");
2874 assert!(!nodes.iter().any(|n| n.kind == "emph" || n.kind == "strong"));
2875 let text: String = nodes
2876 .iter()
2877 .filter(|n| n.kind == "str")
2878 .filter_map(|n| n.text.clone())
2879 .collect();
2880 assert_eq!(text, "*hi*z");
2881 }
2882 }
2883
2884 #[test]
2885 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
2886 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
2888 ed.insert_literal(1, "# ").expect("literal");
2889 assert_eq!(ed.source_str().unwrap(), "a# z\n");
2890
2891 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
2893 ed2.insert_literal(0, "# ").expect("literal");
2894 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
2895 assert!(!ed2.nodes().expect("nodes").iter().any(|n| n.kind == "heading"));
2896 }
2897
2898 #[test]
2899 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
2900 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
2901 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
2902
2903 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2904 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
2905 }
2906
2907 #[test]
2908 fn editor_insert_line_break_splices_in_cell_br() {
2909 let mut ed =
2910 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
2911 ed.insert_line_break(3).expect("line break");
2913 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
2914 let nodes = ed.nodes().expect("nodes");
2916 assert!(nodes.iter().any(|n| n.kind == "hard_break"));
2917 assert!(!nodes.iter().any(|n| n.kind == "raw_inline"));
2918 }
2919
2920 #[test]
2921 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
2922 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
2924 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
2925
2926 let mut dj =
2928 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
2929 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
2930
2931 let mut ed =
2933 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
2934 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
2935 }
2936
2937 #[test]
2938 fn editor_undo_redo_round_trip() {
2939 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2940 ed.edit_range(5, 5, "!").expect("edit");
2941 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2942
2943 let change = ed.undo().expect("undo ok").expect("something to undo");
2944 assert_eq!(ed.source_str().unwrap(), "hello\n");
2945 assert_eq!(change.new.end, 5);
2946 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
2947
2948 ed.redo().expect("redo ok").expect("something to redo");
2949 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2950 }
2951
2952 #[test]
2953 fn editor_coalesce_folds_a_run() {
2954 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2955 ed.edit_range(0, 0, "a").expect("edit");
2956 ed.edit_range(1, 1, "b").expect("edit");
2957 ed.coalesce_last_undo().expect("coalesce");
2958 assert_eq!(ed.source_str().unwrap(), "ab\n");
2959 ed.undo().expect("undo ok").expect("something to undo");
2961 assert_eq!(ed.source_str().unwrap(), "\n");
2962 assert!(ed.undo().expect("undo ok").is_none());
2963 }
2964
2965 #[test]
2966 fn editor_revision_bumps_per_successful_mutation() {
2967 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
2968 assert_eq!(ed.revision(), 0);
2969 ed.edit_range(1, 1, "y").expect("edit");
2970 assert_eq!(ed.revision(), 1);
2971
2972 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
2974 assert_eq!(xml.revision(), 0);
2975 assert!(xml.replace_content("0", "<b>").is_err());
2976 assert_eq!(xml.revision(), 0);
2977
2978 ed.undo().expect("undo ok").expect("something to undo");
2980 assert_eq!(ed.revision(), 2);
2981 ed.redo().expect("redo ok").expect("something to redo");
2982 assert_eq!(ed.revision(), 3);
2983 }
2984
2985 #[test]
2986 fn editor_dirty_range_tracks_and_clears() {
2987 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
2988 assert_eq!(ed.dirty_range(), None);
2990
2991 ed.edit_range(2, 2, "XY").expect("edit");
2993 assert_eq!(ed.dirty_range(), Some(2..4));
2994
2995 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
2999 assert!(d.start <= 2 && d.end >= 10, "range {d:?} must cover both edits");
3000
3001 let rev = ed.revision();
3003 ed.clear_dirty();
3004 assert_eq!(ed.dirty_range(), None);
3005 assert_eq!(ed.revision(), rev);
3006
3007 ed.undo().expect("undo ok").expect("something to undo");
3009 assert!(ed.dirty_range().is_some());
3010 }
3011
3012 #[test]
3013 fn editor_caret_blob_follows_undo_and_redo() {
3014 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
3015 assert!(ed.caret_blob().unwrap().is_empty());
3016
3017 ed.set_caret_blob(b"before").expect("set caret");
3019 ed.edit_range(5, 5, "!").expect("edit");
3020 assert!(ed.caret_blob().unwrap().is_empty());
3022 ed.set_caret_blob(b"after").expect("set caret");
3023
3024 ed.undo().expect("undo ok").expect("something to undo");
3026 assert_eq!(ed.source_str().unwrap(), "hello\n");
3027 assert_eq!(ed.caret_blob().unwrap(), b"before");
3028
3029 ed.redo().expect("redo ok").expect("something to redo");
3031 assert_eq!(ed.source_str().unwrap(), "hello!\n");
3032 assert_eq!(ed.caret_blob().unwrap(), b"after");
3033 }
3034
3035 #[test]
3036 fn editor_coalesced_run_keeps_the_pre_run_caret() {
3037 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
3038 ed.set_caret_blob(b"c0").expect("set caret");
3039 ed.edit_range(0, 0, "a").expect("edit");
3040 ed.set_caret_blob(b"c1").expect("set caret");
3041 ed.edit_range(1, 1, "b").expect("edit");
3042 ed.coalesce_last_undo().expect("coalesce");
3043 ed.set_caret_blob(b"c2").expect("set caret");
3044
3045 ed.undo().expect("undo ok").expect("something to undo");
3047 assert_eq!(ed.source_str().unwrap(), "\n");
3048 assert_eq!(ed.caret_blob().unwrap(), b"c0");
3049 }
3050
3051 #[test]
3052 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
3053 let mut ed =
3054 Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
3055 ed.renumber_ordered_lists(0).expect("renumber ok");
3056 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
3057 }
3058
3059 #[test]
3060 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
3061 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
3062 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
3063 }
3064
3065 #[test]
3066 fn editor_table_insert_row_and_set_alignment() {
3067 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
3068 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
3069 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
3071 ed.source_str().unwrap(),
3072 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
3073 );
3074 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
3076 }
3077
3078 #[test]
3079 fn editor_table_edit_off_a_table_is_not_found() {
3080 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
3081 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
3082 }
3083
3084 #[test]
3085 fn editor_set_block_converts_setext_heading() {
3086 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
3088 ed.set_block(0, BlockKind::Heading(1)).expect("setext to atx");
3089 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
3090 }
3091
3092 #[test]
3093 fn editor_unwrap_and_smart_delete() {
3094 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
3095 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
3097
3098 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
3099 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
3101 }
3102
3103 #[test]
3104 fn editor_directives_require_the_extension_flag() {
3105 let src = ":::vis{.public}\nhi\n:::\n";
3106 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
3109 assert_eq!(plain.query("directive").expect("query").len(), 0);
3110 let mut ext = Editor::new_ext(
3112 src.as_bytes(),
3113 Format::Markdown,
3114 MarkdownExtensions { directives: true, ..Default::default() },
3115 )
3116 .expect("editor");
3117 assert_eq!(ext.query("directive").expect("query").len(), 1);
3118 }
3119
3120 #[test]
3121 fn document_html_elements_make_embedded_img_queryable() {
3122 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
3123 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
3125 assert_eq!(plain.query("image").expect("query").len(), 0);
3126 let mut ext = Document::parse_str_with(
3128 src,
3129 Format::Markdown,
3130 MarkdownExtensions { html_elements: true, ..Default::default() },
3131 )
3132 .expect("parse");
3133 let images = ext.query("image").expect("query");
3134 assert_eq!(images.len(), 1);
3135 assert_eq!(images[0].kind, "image");
3136 }
3137
3138 #[test]
3139 fn editor_filter_public_audience_view() {
3140 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
3141 let mut ed = Editor::new_ext(
3142 src.as_bytes(),
3143 Format::Markdown,
3144 MarkdownExtensions { directives: true, ..Default::default() },
3145 )
3146 .expect("editor");
3147 ed.filter(
3149 "directive[name=vis]",
3150 Some("directive[class~=public]"),
3151 true,
3152 )
3153 .expect("filter");
3154 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
3155 }
3156
3157 #[test]
3158 fn editor_filter_rejects_a_malformed_selector() {
3159 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
3160 assert_eq!(ed.filter("list >", None, false), Err(Error::InvalidArgument));
3161 }
3162
3163 #[test]
3164 fn builder_builds_and_renders_a_document() {
3165 let mut b = Builder::new().expect("builder");
3166
3167 let title = b.add_text(TextKind::Str, "Title").unwrap();
3169 let heading = b.add_heading(1).unwrap();
3170 b.set_children(heading, &[title]).unwrap();
3171
3172 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
3173 let world = b.add_text(TextKind::Str, "world").unwrap();
3174 let emph = b.add(VoidKind::Emph).unwrap();
3175 b.set_children(emph, &[world]).unwrap();
3176 let para = b.add(VoidKind::Para).unwrap();
3177 b.set_children(para, &[hello, emph]).unwrap();
3178
3179 let doc = b.add(VoidKind::Doc).unwrap();
3180 b.set_children(doc, &[heading, para]).unwrap();
3181
3182 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
3183 assert!(html.contains("<h1>Title</h1>"), "{html}");
3184 assert!(html.contains("<em>world</em>"), "{html}");
3185
3186 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
3187 assert!(md.contains("# Title"), "{md}");
3188 assert!(md.contains("*world*"), "{md}");
3189
3190 let matches = b.query(doc, "heading").unwrap();
3191 assert_eq!(matches.len(), 1);
3192 assert_eq!(matches[0].kind, "heading");
3193
3194 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
3195 assert!(json.contains("\"kind\": \"doc\""), "{json}");
3196 }
3197
3198 #[test]
3199 fn builder_element_with_attributes() {
3200 let mut b = Builder::new().expect("builder");
3201 let inner = b.add_text(TextKind::Str, "hi").unwrap();
3202 let el = b.add_element("section").unwrap();
3203 b.set_children(el, &[inner]).unwrap();
3204 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)]).unwrap();
3205
3206 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
3207 assert!(html.contains("<section"), "{html}");
3208 assert!(html.contains("class=\"note\""), "{html}");
3209 assert!(html.contains("hidden"), "{html}");
3210 }
3211
3212 #[test]
3213 fn builder_lists_round_trip_to_markdown() {
3214 let mut b = Builder::new().expect("builder");
3215
3216 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
3218 let one_para = b.add(VoidKind::Para).unwrap();
3219 b.set_children(one_para, &[one_txt]).unwrap();
3220 let one = b.add(VoidKind::ListItem).unwrap();
3221 b.set_children(one, &[one_para]).unwrap();
3222
3223 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
3224 let two_para = b.add(VoidKind::Para).unwrap();
3225 b.set_children(two_para, &[two_txt]).unwrap();
3226 let two = b.add(VoidKind::ListItem).unwrap();
3227 b.set_children(two, &[two_para]).unwrap();
3228
3229 let list = b
3230 .add_ordered_list(OrderedNumbering::Decimal, OrderedDelim::Period, true, Some(1))
3231 .unwrap();
3232 b.set_children(list, &[one, two]).unwrap();
3233 let doc = b.add(VoidKind::Doc).unwrap();
3234 b.set_children(doc, &[list]).unwrap();
3235
3236 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
3237 assert!(md.contains("1. one"), "{md}");
3238 assert!(md.contains("2. two"), "{md}");
3239 }
3240
3241 #[test]
3242 fn builder_rejects_invalid_kind_and_id() {
3243 let b = Builder::new().expect("builder");
3244 let mut id = 0u32;
3248 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
3249 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
3250
3251 let mut ptr = std::ptr::null();
3253 let mut len = 0usize;
3254 let status = unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
3255 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
3256 }
3257}