1mod error;
2
3pub(crate) use twig_sys as ffi;
8
9use std::ops::Range;
10use std::os::raw::{c_char, c_int};
11use std::ptr::NonNull;
12
13pub use error::Error;
14pub use ffi::TwigSpan as Span;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum Format {
18 Djot,
19 Markdown,
20 Xml,
21 Html,
22}
23
24impl From<Format> for ffi::TwigFormat {
25 fn from(value: Format) -> Self {
26 match value {
27 Format::Djot => ffi::TwigFormat::Djot,
28 Format::Markdown => ffi::TwigFormat::Markdown,
29 Format::Xml => ffi::TwigFormat::Xml,
30 Format::Html => ffi::TwigFormat::Html,
31 }
32 }
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct QueryMatch {
38 pub node_id: u32,
40 pub span: Range<usize>,
42 pub content_span: Option<Range<usize>>,
45 pub kind: String,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct Change {
57 pub old: Range<usize>,
58 pub new: Range<usize>,
59}
60
61impl Change {
62 pub fn delta(&self) -> isize {
64 self.new.len() as isize - self.old.len() as isize
65 }
66
67 fn from_ffi(c: ffi::TwigChange) -> Self {
68 Change {
69 old: c.old_span.start..c.old_span.end,
70 new: c.new_span.start..c.new_span.end,
71 }
72 }
73}
74
75#[derive(Clone, Debug, Eq, PartialEq)]
86#[non_exhaustive]
87pub struct FlatNode {
88 pub id: NodeId,
89 pub parent: Option<NodeId>,
90 pub first_child: Option<NodeId>,
91 pub next_sibling: Option<NodeId>,
92 pub span: Range<usize>,
93 pub content_span: Option<Range<usize>>,
94 pub level: Option<u32>,
96 pub kind: String,
97 pub text: Option<String>,
98 pub destination: Option<String>,
99 pub head: Option<bool>,
102 pub alignment: Option<Alignment>,
108 pub name: Option<String>,
116 pub directive_form: Option<DirectiveForm>,
123 pub attrs: Vec<(String, Option<String>)>,
127}
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub enum InlineKind {
135 Strong,
136 Emph,
137 Verbatim,
138 Mark,
139 Superscript,
140 Subscript,
141 Insert,
142 Delete,
143}
144
145impl InlineKind {
146 fn to_c(self) -> c_int {
147 match self {
148 InlineKind::Strong => 0,
149 InlineKind::Emph => 1,
150 InlineKind::Verbatim => 2,
151 InlineKind::Mark => 3,
152 InlineKind::Superscript => 4,
153 InlineKind::Subscript => 5,
154 InlineKind::Insert => 6,
155 InlineKind::Delete => 7,
156 }
157 }
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum BlockKind {
163 Paragraph,
164 Heading(u32),
166}
167
168impl BlockKind {
169 fn to_c(self) -> (c_int, u32) {
171 match self {
172 BlockKind::Paragraph => (0, 0),
173 BlockKind::Heading(level) => (1, level),
174 }
175 }
176}
177
178#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub enum BlockContainerKind {
185 BlockQuote,
186 BulletList,
187 OrderedList,
188}
189
190impl BlockContainerKind {
191 fn to_c(self) -> c_int {
192 match self {
193 BlockContainerKind::BlockQuote => 0,
194 BlockContainerKind::BulletList => 1,
195 BlockContainerKind::OrderedList => 2,
196 }
197 }
198}
199
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201pub struct Version {
202 pub major: u8,
203 pub minor: u8,
204 pub patch: u8,
205}
206
207pub fn version() -> Version {
208 let packed = unsafe { ffi::twig_version() };
209 Version {
210 major: (packed >> 16) as u8,
211 minor: (packed >> 8) as u8,
212 patch: packed as u8,
213 }
214}
215
216pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
222
223pub fn abi_version() -> u32 {
229 unsafe { ffi::twig_abi_version() }
230}
231
232pub fn version_string() -> &'static str {
233 let ptr = unsafe { ffi::twig_version_string() };
234 unsafe { std::ffi::CStr::from_ptr(ptr) }
235 .to_str()
236 .unwrap_or("")
237}
238
239#[derive(Debug)]
240pub struct Document {
241 raw: NonNull<ffi::TwigDocument>,
242}
243
244impl Document {
245 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
246 Self::parse_with(input, format, MarkdownExtensions::default())
247 }
248
249 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
250 Self::parse(input.as_bytes(), format)
251 }
252
253 pub fn parse_with(
259 input: &[u8],
260 format: Format,
261 extensions: MarkdownExtensions,
262 ) -> Result<Self, Error> {
263 let mut raw = std::ptr::null_mut();
264 let ffi_format: ffi::TwigFormat = format.into();
265 let status = unsafe {
266 ffi::twig_parse_ext(
267 input.as_ptr(),
268 input.len(),
269 ffi_format as i32,
270 extensions.to_flags(),
271 &mut raw,
272 )
273 };
274 Error::from_status(status)?;
275 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
276 Ok(Self { raw })
277 }
278
279 pub fn parse_str_with(
281 input: &str,
282 format: Format,
283 extensions: MarkdownExtensions,
284 ) -> Result<Self, Error> {
285 Self::parse_with(input.as_bytes(), format, extensions)
286 }
287
288 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
291 let raw = self.raw.as_ptr();
292 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
293 }
294
295 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
301 let raw = self.raw.as_ptr();
302 let ffi_format: ffi::TwigFormat = format.into();
303 collect_bytes(|ptr, len| unsafe {
304 ffi::twig_document_serialize(raw, ffi_format as i32, ptr, len)
305 })
306 }
307
308 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
311 let raw = self.raw.as_ptr();
312 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
313 }
314
315 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
324 let raw = self.raw.as_ptr();
325 collect_matches(|ptr, len| unsafe {
326 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
327 })
328 }
329}
330
331impl Drop for Document {
332 fn drop(&mut self) {
333 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
334 }
335}
336
337#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
343pub struct MarkdownExtensions {
344 pub directives: bool,
346 pub math: bool,
348 pub html_elements: bool,
353}
354
355impl MarkdownExtensions {
356 fn to_flags(self) -> u32 {
357 let mut flags = 0;
358 if self.directives {
359 flags |= ffi::TWIG_MD_DIRECTIVES;
360 }
361 if self.math {
362 flags |= ffi::TWIG_MD_MATH;
363 }
364 if self.html_elements {
365 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
366 }
367 flags
368 }
369}
370
371#[derive(Debug)]
377pub struct Editor {
378 raw: NonNull<ffi::TwigEditor>,
379}
380
381impl Editor {
382 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
385 let mut raw = std::ptr::null_mut();
386 let ffi_format: ffi::TwigFormat = format.into();
387 let status =
388 unsafe { ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
389 Error::from_status(status)?;
390 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
391 Ok(Self { raw })
392 }
393
394 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
395 Self::new(input.as_bytes(), format)
396 }
397
398 pub fn new_ext(input: &[u8], format: Format, extensions: MarkdownExtensions) -> Result<Self, Error> {
403 let mut raw = std::ptr::null_mut();
404 let ffi_format: ffi::TwigFormat = format.into();
405 let status = unsafe {
406 ffi::twig_editor_create_ext(
407 input.as_ptr(),
408 input.len(),
409 ffi_format as i32,
410 extensions.to_flags(),
411 &mut raw,
412 )
413 };
414 Error::from_status(status)?;
415 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
416 Ok(Self { raw })
417 }
418
419 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
421 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
422 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
423 })
424 }
425
426 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
429 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
430 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
431 })
432 }
433
434 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
436 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
437 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
438 })
439 }
440
441 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
443 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
444 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
445 })
446 }
447
448 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
451 let status = unsafe {
452 ffi::twig_editor_insert_child(
453 self.raw.as_ptr(),
454 locator.as_ptr(),
455 locator.len(),
456 index,
457 text.as_ptr(),
458 text.len(),
459 )
460 };
461 Error::from_status(status)
462 }
463
464 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
467 let status = unsafe {
468 ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len())
469 };
470 Error::from_status(status)
471 }
472
473 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
476 let status = unsafe {
477 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
478 };
479 Error::from_status(status)
480 }
481
482 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
486 let status = unsafe {
487 ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len())
488 };
489 Error::from_status(status)
490 }
491
492 pub fn filter(&mut self, drop: &str, keep: Option<&str>, unwrap_kept: bool) -> Result<(), Error> {
497 let (keep_ptr, keep_len) = match keep {
498 Some(k) => (k.as_ptr(), k.len()),
499 None => (std::ptr::null(), 0),
500 };
501 let status = unsafe {
502 ffi::twig_editor_filter(
503 self.raw.as_ptr(),
504 drop.as_ptr(),
505 drop.len(),
506 keep_ptr,
507 keep_len,
508 unwrap_kept as i32,
509 )
510 };
511 Error::from_status(status)
512 }
513
514 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
516 let raw = self.raw.as_ptr();
517 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
518 }
519
520 pub fn source_str(&mut self) -> Result<String, Error> {
522 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
523 }
524
525 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
528 let raw = self.raw.as_ptr();
529 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
530 }
531
532 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
535 let raw = self.raw.as_ptr();
536 collect_matches(|ptr, len| unsafe {
537 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
538 })
539 }
540
541 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
551 let mut change = ffi::TwigChange {
552 old_span: ffi::TwigSpan { start: 0, end: 0 },
553 new_span: ffi::TwigSpan { start: 0, end: 0 },
554 };
555 let status = unsafe {
556 ffi::twig_editor_edit_range(
557 self.raw.as_ptr(),
558 start,
559 end,
560 text.as_ptr(),
561 text.len(),
562 &mut change,
563 )
564 };
565 Error::from_status(status)?;
566 Ok(Change::from_ffi(change))
567 }
568
569 pub fn last_change(&mut self) -> Option<Change> {
575 let mut change = ffi::TwigChange {
576 old_span: ffi::TwigSpan { start: 0, end: 0 },
577 new_span: ffi::TwigSpan { start: 0, end: 0 },
578 };
579 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
580 match status.0 {
581 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
582 _ => None,
583 }
584 }
585
586 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
591 let mut change = ffi::TwigChange {
592 old_span: ffi::TwigSpan { start: 0, end: 0 },
593 new_span: ffi::TwigSpan { start: 0, end: 0 },
594 };
595 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
596 if status.0 == ffi::TwigStatus::NOT_FOUND {
597 return Ok(None);
598 }
599 Error::from_status(status)?;
600 Ok(Some(Change::from_ffi(change)))
601 }
602
603 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
607 let mut change = ffi::TwigChange {
608 old_span: ffi::TwigSpan { start: 0, end: 0 },
609 new_span: ffi::TwigSpan { start: 0, end: 0 },
610 };
611 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
612 if status.0 == ffi::TwigStatus::NOT_FOUND {
613 return Ok(None);
614 }
615 Error::from_status(status)?;
616 Ok(Some(Change::from_ffi(change)))
617 }
618
619 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
624 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
625 Error::from_status(status)
626 }
627
628 pub fn revision(&mut self) -> u64 {
634 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
635 }
636
637 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
658 let mut span = ffi::TwigSpan { start: 0, end: 0 };
659 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
660 match status.0 {
661 ffi::TwigStatus::OK => Some(span.start..span.end),
662 _ => None,
663 }
664 }
665
666 pub fn clear_dirty(&mut self) {
671 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
672 }
673
674 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
682 let status =
683 unsafe { ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len()) };
684 Error::from_status(status)
685 }
686
687 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
692 let raw = self.raw.as_ptr();
693 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
694 }
695
696 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
701 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
702 let mut len = 0usize;
703 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
704 Error::from_status(status)?;
705 if len == 0 {
706 return Ok(Vec::new());
707 }
708 if ptr.is_null() {
709 return Err(Error::Internal);
710 }
711 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
712 raw.iter().map(flat_node_from_ffi).collect()
713 }
714
715 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
722 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
723 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
724 let mut len = 0usize;
725 let status =
726 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
727 Error::from_status(status)?;
728 if len == 0 || ptr.is_null() {
729 return Ok(Vec::new());
730 }
731 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
732 raw.iter().map(query_match_from_ffi).collect()
733 }
734
735 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
743 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
744 let mut len = 0usize;
745 let status =
746 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
747 Error::from_status(status)?;
748 if len == 0 || ptr.is_null() {
749 return Ok(Vec::new());
750 }
751 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
752 raw.iter().map(flat_node_from_ffi).collect()
753 }
754
755 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
760 let mut m = ffi::TwigQueryMatch {
761 node_id: 0,
762 span: ffi::TwigSpan { start: 0, end: 0 },
763 content_span: ffi::TwigSpan { start: 0, end: 0 },
764 has_content_span: 0,
765 kind: std::ptr::null(),
766 };
767 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
768 match status.0 {
769 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
770 ffi::TwigStatus::NOT_FOUND => Ok(None),
771 _ => Err(Error::from_status(status).unwrap_err()),
772 }
773 }
774
775 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
779 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
780 let mut len = 0usize;
781 let status =
782 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
783 match status.0 {
784 ffi::TwigStatus::OK => {}
785 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
786 _ => return Err(Error::from_status(status).unwrap_err()),
787 }
788 if len == 0 || ptr.is_null() {
789 return Ok(Vec::new());
790 }
791 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
792 raw.iter().map(query_match_from_ffi).collect()
793 }
794
795 pub fn wrap_range(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
803 self.change_op(|ed, out| unsafe {
804 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
805 })
806 }
807
808 pub fn toggle_inline(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
813 self.change_op(|ed, out| unsafe {
814 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
815 })
816 }
817
818 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
825 let (block_kind, level) = kind.to_c();
826 self.change_op(|ed, out| unsafe {
827 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
828 })
829 }
830
831 pub fn toggle_block_container(
854 &mut self,
855 start: usize,
856 end: usize,
857 kind: BlockContainerKind,
858 ) -> Result<Change, Error> {
859 self.change_op(|ed, out| unsafe {
860 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
861 })
862 }
863
864 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
876 self.change_op(|ed, out| unsafe {
877 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
878 })?;
879 Ok(())
880 }
881
882 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
891 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
892 }
893
894 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
897 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
898 }
899
900 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
902 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
903 }
904
905 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
907 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
908 }
909
910 pub fn table_set_alignment(&mut self, offset: usize, alignment: Alignment) -> Result<(), Error> {
912 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
913 }
914
915 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
917 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
918 }
919
920 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
922 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
923 }
924
925 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
926 self.change_op(|ed, out| unsafe {
927 ffi::twig_editor_table_edit(ed, offset, op, arg, out)
928 })?;
929 Ok(())
930 }
931
932 pub fn insert_link(
977 &mut self,
978 start: usize,
979 end: usize,
980 destination: &str,
981 ) -> Result<Change, Error> {
982 self.change_op(|ed, out| unsafe {
983 ffi::twig_editor_insert_link(
984 ed,
985 start,
986 end,
987 destination.as_ptr(),
988 destination.len(),
989 out,
990 )
991 })
992 }
993
994 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
1015 self.change_op(|ed, out| unsafe {
1016 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
1017 })
1018 }
1019
1020 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
1034 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
1035 }
1036
1037 fn change_op(
1040 &mut self,
1041 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
1042 ) -> Result<Change, Error> {
1043 let mut change = ffi::TwigChange {
1044 old_span: ffi::TwigSpan { start: 0, end: 0 },
1045 new_span: ffi::TwigSpan { start: 0, end: 0 },
1046 };
1047 let status = op(self.raw.as_ptr(), &mut change);
1048 Error::from_status(status)?;
1049 Ok(Change::from_ffi(change))
1050 }
1051
1052 fn apply(
1054 &mut self,
1055 locator: &str,
1056 text: &str,
1057 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
1058 ) -> Result<(), Error> {
1059 let status = op(
1060 self.raw.as_ptr(),
1061 locator.as_ptr(),
1062 locator.len(),
1063 text.as_ptr(),
1064 text.len(),
1065 );
1066 Error::from_status(status)
1067 }
1068}
1069
1070impl Drop for Editor {
1071 fn drop(&mut self) {
1072 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
1073 }
1074}
1075
1076fn collect_bytes(
1081 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
1082) -> Result<Vec<u8>, Error> {
1083 let mut ptr = std::ptr::null();
1084 let mut len = 0usize;
1085 let status = call(&mut ptr, &mut len);
1086 Error::from_status(status)?;
1087 if len == 0 {
1088 return Ok(Vec::new());
1089 }
1090 if ptr.is_null() {
1091 return Err(Error::Internal);
1092 }
1093 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1094 Ok(bytes.to_vec())
1095}
1096
1097fn collect_matches(
1100 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
1101) -> Result<Vec<QueryMatch>, Error> {
1102 let mut ptr = std::ptr::null();
1103 let mut len = 0usize;
1104 let status = call(&mut ptr, &mut len);
1105 Error::from_status(status)?;
1106 if len == 0 {
1107 return Ok(Vec::new());
1108 }
1109 if ptr.is_null() {
1110 return Err(Error::Internal);
1111 }
1112 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1113 matches.iter().map(query_match_from_ffi).collect()
1114}
1115
1116fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
1119 Ok(QueryMatch {
1120 node_id: m.node_id,
1121 span: m.span.start..m.span.end,
1122 content_span: if m.has_content_span != 0 {
1123 Some(m.content_span.start..m.content_span.end)
1124 } else {
1125 None
1126 },
1127 kind: borrowed_cstr(m.kind)?,
1128 })
1129}
1130
1131fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
1133 let node_id = |v: u32| if v == ffi::TWIG_NO_NODE { None } else { Some(NodeId(v)) };
1134 Ok(FlatNode {
1135 id: NodeId(n.id),
1136 parent: node_id(n.parent),
1137 first_child: node_id(n.first_child),
1138 next_sibling: node_id(n.next_sibling),
1139 span: n.span.start..n.span.end,
1140 content_span: if n.has_content_span != 0 {
1141 Some(n.content_span.start..n.content_span.end)
1142 } else {
1143 None
1144 },
1145 level: if n.level != 0 { Some(n.level) } else { None },
1146 kind: borrowed_cstr(n.kind)?,
1147 text: borrowed_bytes(n.text_ptr, n.text_len),
1148 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
1149 head: match n.head {
1150 ffi::TWIG_HEAD_NONE => None,
1151 v => Some(v != 0),
1152 },
1153 alignment: Alignment::from_c(n.alignment),
1154 name: borrowed_bytes(n.name_ptr, n.name_len),
1155 directive_form: DirectiveForm::from_c(n.directive_form),
1156 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
1157 })
1158}
1159
1160fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
1164 if ptr.is_null() || len == 0 {
1165 return Vec::new();
1166 }
1167 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
1168 kvs.iter()
1169 .map(|kv| {
1170 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
1171 (key, borrowed_bytes(kv.value, kv.value_len))
1172 })
1173 .collect()
1174}
1175
1176fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
1178 if ptr.is_null() {
1179 return Err(Error::Internal);
1180 }
1181 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
1182 .to_str()
1183 .map_err(|_| Error::Internal)?
1184 .to_owned())
1185}
1186
1187fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
1191 if ptr.is_null() {
1192 return None;
1193 }
1194 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1195 Some(String::from_utf8_lossy(bytes).into_owned())
1196}
1197
1198#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1202pub struct NodeId(pub u32);
1203
1204#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1207pub enum VoidKind {
1208 Doc,
1209 Para,
1210 ThematicBreak,
1211 Section,
1212 Div,
1213 BlockQuote,
1214 DefinitionList,
1215 Table,
1216 ListItem,
1217 DefinitionListItem,
1218 Term,
1219 Definition,
1220 Caption,
1221 SoftBreak,
1222 HardBreak,
1223 NonBreakingSpace,
1224 Emph,
1225 Strong,
1226 Span,
1227 Mark,
1228 Superscript,
1229 Subscript,
1230 Insert,
1231 Delete,
1232 DoubleQuoted,
1233 SingleQuoted,
1234}
1235
1236impl VoidKind {
1237 fn to_c(self) -> c_int {
1238 match self {
1240 VoidKind::Doc => 0,
1241 VoidKind::Para => 1,
1242 VoidKind::ThematicBreak => 3,
1243 VoidKind::Section => 4,
1244 VoidKind::Div => 5,
1245 VoidKind::BlockQuote => 9,
1246 VoidKind::DefinitionList => 13,
1247 VoidKind::Table => 14,
1248 VoidKind::ListItem => 15,
1249 VoidKind::DefinitionListItem => 17,
1250 VoidKind::Term => 18,
1251 VoidKind::Definition => 19,
1252 VoidKind::Caption => 22,
1253 VoidKind::SoftBreak => 26,
1254 VoidKind::HardBreak => 27,
1255 VoidKind::NonBreakingSpace => 28,
1256 VoidKind::Emph => 38,
1257 VoidKind::Strong => 39,
1258 VoidKind::Span => 42,
1259 VoidKind::Mark => 43,
1260 VoidKind::Superscript => 44,
1261 VoidKind::Subscript => 45,
1262 VoidKind::Insert => 46,
1263 VoidKind::Delete => 47,
1264 VoidKind::DoubleQuoted => 48,
1265 VoidKind::SingleQuoted => 49,
1266 }
1267 }
1268}
1269
1270#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1272pub enum TextKind {
1273 Str,
1274 Symb,
1275 Verbatim,
1276 InlineMath,
1277 DisplayMath,
1278 Url,
1279 Email,
1280 FootnoteReference,
1281 Comment,
1282 Doctype,
1283 Cdata,
1284}
1285
1286impl TextKind {
1287 fn to_c(self) -> c_int {
1288 match self {
1289 TextKind::Str => 25,
1290 TextKind::Symb => 29,
1291 TextKind::Verbatim => 30,
1292 TextKind::InlineMath => 32,
1293 TextKind::DisplayMath => 33,
1294 TextKind::Url => 34,
1295 TextKind::Email => 35,
1296 TextKind::FootnoteReference => 36,
1297 TextKind::Comment => 52,
1298 TextKind::Doctype => 53,
1299 TextKind::Cdata => 55,
1300 }
1301 }
1302}
1303
1304#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1306pub enum BulletStyle {
1307 Dash,
1308 Plus,
1309 Star,
1310}
1311
1312impl BulletStyle {
1313 fn to_c(self) -> c_int {
1314 match self {
1315 BulletStyle::Dash => 0,
1316 BulletStyle::Plus => 1,
1317 BulletStyle::Star => 2,
1318 }
1319 }
1320}
1321
1322#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1324pub enum OrderedNumbering {
1325 Decimal,
1326 LowerAlpha,
1327 UpperAlpha,
1328 LowerRoman,
1329 UpperRoman,
1330}
1331
1332impl OrderedNumbering {
1333 fn to_c(self) -> c_int {
1334 match self {
1335 OrderedNumbering::Decimal => 0,
1336 OrderedNumbering::LowerAlpha => 1,
1337 OrderedNumbering::UpperAlpha => 2,
1338 OrderedNumbering::LowerRoman => 3,
1339 OrderedNumbering::UpperRoman => 4,
1340 }
1341 }
1342}
1343
1344#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1346pub enum OrderedDelim {
1347 Period,
1348 ParenAfter,
1349 ParenBoth,
1350}
1351
1352impl OrderedDelim {
1353 fn to_c(self) -> c_int {
1354 match self {
1355 OrderedDelim::Period => 0,
1356 OrderedDelim::ParenAfter => 1,
1357 OrderedDelim::ParenBoth => 2,
1358 }
1359 }
1360}
1361
1362#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1365pub enum Alignment {
1366 Default,
1367 Left,
1368 Right,
1369 Center,
1370}
1371
1372impl Alignment {
1373 fn to_c(self) -> c_int {
1374 match self {
1375 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
1376 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
1377 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
1378 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
1379 }
1380 }
1381
1382 fn from_c(v: c_int) -> Option<Self> {
1385 match v {
1386 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
1387 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
1388 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
1389 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
1390 _ => None,
1391 }
1392 }
1393}
1394
1395#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1397pub enum SmartPunctuation {
1398 LeftSingleQuote,
1399 RightSingleQuote,
1400 LeftDoubleQuote,
1401 RightDoubleQuote,
1402 Ellipses,
1403 EmDash,
1404 EnDash,
1405}
1406
1407impl SmartPunctuation {
1408 fn to_c(self) -> c_int {
1409 match self {
1410 SmartPunctuation::LeftSingleQuote => 0,
1411 SmartPunctuation::RightSingleQuote => 1,
1412 SmartPunctuation::LeftDoubleQuote => 2,
1413 SmartPunctuation::RightDoubleQuote => 3,
1414 SmartPunctuation::Ellipses => 4,
1415 SmartPunctuation::EmDash => 5,
1416 SmartPunctuation::EnDash => 6,
1417 }
1418 }
1419}
1420
1421#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1423pub enum DirectiveForm {
1424 Text,
1425 Leaf,
1426 Container,
1427}
1428
1429impl DirectiveForm {
1430 fn to_c(self) -> c_int {
1431 match self {
1432 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
1433 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
1434 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
1435 }
1436 }
1437
1438 fn from_c(v: c_int) -> Option<Self> {
1442 match v {
1443 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
1444 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
1445 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
1446 _ => None,
1447 }
1448 }
1449}
1450
1451fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
1455 match s {
1456 Some(x) => (x.as_ptr(), x.len(), 1),
1457 None => (std::ptr::null(), 0, 0),
1458 }
1459}
1460
1461#[derive(Debug)]
1468pub struct Builder {
1469 raw: NonNull<ffi::TwigBuilder>,
1470}
1471
1472impl Builder {
1473 pub fn new() -> Result<Self, Error> {
1475 let mut raw = std::ptr::null_mut();
1476 let status = unsafe { ffi::twig_builder_create(&mut raw) };
1477 Error::from_status(status)?;
1478 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1479 Ok(Self { raw })
1480 }
1481
1482 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
1485 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
1486 }
1487
1488 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
1490 self.emit(|b, out| unsafe { ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out) })
1491 }
1492
1493 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
1495 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
1496 }
1497
1498 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
1500 let (lp, ll, has) = opt_str(lang);
1501 self.emit(|b, out| unsafe { ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out) })
1502 }
1503
1504 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1506 self.emit(|b, out| unsafe {
1507 ffi::twig_builder_add_raw_block(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1508 })
1509 }
1510
1511 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
1513 self.emit(|b, out| unsafe {
1514 ffi::twig_builder_add_metadata(b, lang.as_ptr(), lang.len(), text.as_ptr(), text.len(), out)
1515 })
1516 }
1517
1518 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1520 self.emit(|b, out| unsafe {
1521 ffi::twig_builder_add_raw_inline(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1522 })
1523 }
1524
1525 pub fn add_smart_punctuation(&mut self, kind: SmartPunctuation, text: &str) -> Result<NodeId, Error> {
1527 self.emit(|b, out| unsafe {
1528 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
1529 })
1530 }
1531
1532 pub fn add_link(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1535 let (dp, dl, hd) = opt_str(destination);
1536 let (rp, rl, hr) = opt_str(reference);
1537 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
1538 }
1539
1540 pub fn add_image(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1542 let (dp, dl, hd) = opt_str(destination);
1543 let (rp, rl, hr) = opt_str(reference);
1544 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
1545 }
1546
1547 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
1549 self.emit(|b, out| unsafe { ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out) })
1550 }
1551
1552 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
1554 self.emit(|b, out| unsafe { ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out) })
1555 }
1556
1557 pub fn add_processing_instruction(&mut self, target: &str, data: &str) -> Result<NodeId, Error> {
1559 self.emit(|b, out| unsafe {
1560 ffi::twig_builder_add_processing_instruction(b, target.as_ptr(), target.len(), data.as_ptr(), data.len(), out)
1561 })
1562 }
1563
1564 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
1566 self.emit(|b, out| unsafe { ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out) })
1567 }
1568
1569 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
1571 self.emit(|b, out| unsafe {
1572 ffi::twig_builder_add_reference(b, label.as_ptr(), label.len(), destination.as_ptr(), destination.len(), out)
1573 })
1574 }
1575
1576 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
1578 self.emit(|b, out| unsafe { ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out) })
1579 }
1580
1581 pub fn add_ordered_list(
1583 &mut self,
1584 numbering: OrderedNumbering,
1585 delim: OrderedDelim,
1586 tight: bool,
1587 start: Option<u32>,
1588 ) -> Result<NodeId, Error> {
1589 let (start_val, has_start) = match start {
1590 Some(s) => (s, 1),
1591 None => (0, 0),
1592 };
1593 self.emit(|b, out| unsafe {
1594 ffi::twig_builder_add_ordered_list(b, numbering.to_c(), delim.to_c(), tight as c_int, start_val, has_start, out)
1595 })
1596 }
1597
1598 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
1600 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
1601 }
1602
1603 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
1605 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list_item(b, checked as c_int, out) })
1606 }
1607
1608 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
1610 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
1611 }
1612
1613 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
1615 self.emit(|b, out| unsafe { ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out) })
1616 }
1617
1618 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
1621 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
1622 let status = unsafe { ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len()) };
1623 Error::from_status(status)
1624 }
1625
1626 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
1630 let kvs: Vec<ffi::TwigKeyVal> = attrs
1631 .iter()
1632 .map(|(k, v)| ffi::TwigKeyVal {
1633 key: k.as_ptr(),
1634 key_len: k.len(),
1635 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
1636 value_len: v.map_or(0, |s| s.len()),
1637 })
1638 .collect();
1639 let status = unsafe { ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len()) };
1640 Error::from_status(status)
1641 }
1642
1643 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1646 let raw = self.raw.as_ptr();
1647 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
1648 }
1649
1650 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
1654 let raw = self.raw.as_ptr();
1655 let ffi_format: ffi::TwigFormat = format.into();
1656 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_serialize(raw, root.0, ffi_format as i32, ptr, len) })
1657 }
1658
1659 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1661 let raw = self.raw.as_ptr();
1662 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
1663 }
1664
1665 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1668 let raw = self.raw.as_ptr();
1669 collect_matches(|ptr, len| unsafe {
1670 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
1671 })
1672 }
1673
1674 fn emit(
1677 &mut self,
1678 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
1679 ) -> Result<NodeId, Error> {
1680 let mut id: u32 = 0;
1681 let status = call(self.raw.as_ptr(), &mut id);
1682 Error::from_status(status)?;
1683 Ok(NodeId(id))
1684 }
1685}
1686
1687impl Drop for Builder {
1688 fn drop(&mut self) {
1689 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
1690 }
1691}
1692
1693#[cfg(test)]
1694mod tests {
1695 use super::*;
1696
1697 #[test]
1698 fn abi_version_matches() {
1699 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
1703 }
1704
1705 #[test]
1706 fn parses_and_renders_markdown_html() {
1707 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1708 let html = doc.render_html().expect("render html");
1709 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
1710 }
1711
1712 #[test]
1713 fn parses_html_input() {
1714 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
1715 let html = doc.render_html().expect("render html");
1716 assert!(String::from_utf8_lossy(&html).contains("hi"));
1717 }
1718
1719 #[test]
1720 fn serialize_round_trips_and_cross_converts() {
1721 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1722
1723 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
1724 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
1725
1726 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
1728 }
1729
1730 #[test]
1731 fn serialize_markdown_to_djot() {
1732 let mut doc =
1733 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
1734 let djot = doc.serialize(Format::Djot).expect("serialize djot");
1735 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
1736 }
1737
1738 #[test]
1739 fn ast_json_dumps_the_tree() {
1740 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
1741 let json = doc.ast_json().expect("ast json");
1742 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1743 }
1744
1745 #[test]
1746 fn query_finds_nodes_by_selector() {
1747 let source = "# One\n\n## Two\n";
1748 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1749 let matches = doc.query("heading").expect("query");
1750
1751 assert_eq!(matches.len(), 2);
1752 for m in &matches {
1753 assert_eq!(m.kind, "heading");
1754 assert!(m.span.start < m.span.end);
1755 }
1756 }
1757
1758 #[test]
1759 fn query_recovers_code_spans() {
1760 let source = "prose `code` more prose\n";
1761 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1762 let matches = doc.query("verbatim").expect("query");
1763
1764 assert_eq!(matches.len(), 1);
1765 assert_eq!(&source[matches[0].span.clone()], "`code`");
1766 }
1767
1768 #[test]
1769 fn query_rejects_a_malformed_selector() {
1770 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
1771 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
1772 }
1773
1774 #[test]
1775 fn editor_edits_by_index_path() {
1776 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
1777 ed.replace_content("0.0", "bye").expect("replace_content");
1778 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
1779 }
1780
1781 #[test]
1782 fn flat_nodes_expose_element_name_and_attrs() {
1783 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
1787 let mut ed = Editor::new_ext(
1788 src.as_bytes(),
1789 Format::Markdown,
1790 MarkdownExtensions { html_elements: true, ..Default::default() },
1791 )
1792 .expect("editor");
1793 let nodes = ed.nodes().expect("nodes");
1794
1795 let source = nodes
1796 .iter()
1797 .find(|n| n.name.as_deref() == Some("source"))
1798 .expect("a <source> element node");
1799 assert_eq!(
1800 source.attrs,
1801 vec![
1802 ("media".to_string(), Some("(prefers-color-scheme: dark)".to_string())),
1803 ("srcset".to_string(), Some("d.svg".to_string())),
1804 ]
1805 );
1806
1807 let img = nodes.iter().find(|n| n.kind == "image").expect("an image node");
1810 assert!(img.name.is_none());
1811 assert_eq!(img.destination.as_deref(), Some("l.svg"));
1812
1813 let picture_kids_str = nodes.iter().find(|n| n.kind == "str");
1815 if let Some(s) = picture_kids_str {
1816 assert!(s.name.is_none() && s.attrs.is_empty());
1817 }
1818 }
1819
1820 #[test]
1821 fn flat_nodes_expose_directive_name_and_form() {
1822 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
1828 let mut ed = Editor::new_ext(
1829 src.as_bytes(),
1830 Format::Markdown,
1831 MarkdownExtensions { directives: true, ..Default::default() },
1832 )
1833 .expect("editor");
1834 let nodes = ed.nodes().expect("nodes");
1835
1836 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
1837 .iter()
1838 .filter(|n| n.kind == "directive")
1839 .map(|n| (n.name.as_deref(), n.directive_form))
1840 .collect();
1841 assert_eq!(
1842 forms,
1843 vec![
1844 (Some("note"), Some(DirectiveForm::Container)),
1845 (Some("embed"), Some(DirectiveForm::Leaf)),
1846 (Some("abbr"), Some(DirectiveForm::Text)),
1847 ]
1848 );
1849
1850 let embed = nodes.iter().find(|n| n.name.as_deref() == Some("embed")).expect("embed");
1853 assert_eq!(embed.attrs, vec![("src".to_string(), Some("demo.html".to_string()))]);
1854 let para = nodes.iter().find(|n| n.kind == "para").expect("a para");
1855 assert!(para.directive_form.is_none() && para.name.is_none());
1856 }
1857
1858 #[test]
1859 fn editor_insert_child_and_delete() {
1860 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
1861 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1862 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
1863 ed.delete("0.1").expect("delete");
1864 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
1865 }
1866
1867 #[test]
1868 fn editor_edits_by_selector() {
1869 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1870 ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1871 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
1872 }
1873
1874 #[test]
1875 fn editor_locator_errors_are_distinct() {
1876 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
1877 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
1878 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
1879 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
1880 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
1882 }
1883
1884 #[test]
1885 fn editor_reparse_break_rolls_back() {
1886 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
1887 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
1888 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
1889 }
1890
1891 #[test]
1892 fn editor_leaf_content_is_not_editable() {
1893 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1894 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
1895 }
1896
1897 #[test]
1898 fn editor_query_reflects_current_tree() {
1899 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
1900 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1901 assert_eq!(ed.query("element").expect("query").len(), 3);
1903 let json = ed.ast_json().expect("ast_json");
1904 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1905 }
1906
1907 #[test]
1910 fn editor_edit_range_types_backspaces_and_reports_change() {
1911 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
1912
1913 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
1915 assert_eq!(ed.source_str().unwrap(), "aXb\n");
1916 assert_eq!(c.old, 1..1);
1917 assert_eq!(c.new, 1..2);
1918 assert_eq!(c.delta(), 1);
1919
1920 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
1922 assert_eq!(ed.source_str().unwrap(), "ab\n");
1923 assert_eq!(c2.old, 1..2);
1924 assert_eq!(c2.new, 1..1);
1925 assert_eq!(c2.delta(), -1);
1926 }
1927
1928 #[test]
1929 fn editor_edit_range_rejects_bad_ranges() {
1930 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1931 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"); }
1935
1936 #[test]
1937 fn editor_last_change_reports_locator_ops_too() {
1938 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1939 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1942 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
1943 let c = ed.last_change().expect("a change was recorded");
1944 assert_eq!(c.old, 7..13);
1946 assert_eq!(c.new, 7..17);
1947 }
1948
1949 #[test]
1950 fn editor_nodes_is_a_walkable_flat_tree() {
1951 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1952 let nodes = ed.nodes().expect("nodes");
1953 assert!(!nodes.is_empty());
1954
1955 for (i, n) in nodes.iter().enumerate() {
1957 assert_eq!(n.id, NodeId(i as u32));
1958 }
1959 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
1961 assert_eq!(roots.len(), 1);
1962 assert_eq!(roots[0].kind, "doc");
1963
1964 let heading = nodes.iter().find(|n| n.kind == "heading").expect("a heading");
1966 assert_eq!(heading.level, Some(1));
1967 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
1968
1969 assert_eq!(heading.head, None);
1971 assert_eq!(heading.alignment, None);
1972
1973 for n in nodes.iter().filter(|n| n.parent.is_some()) {
1976 let p = &nodes[n.parent.unwrap().0 as usize];
1977 let mut kid = p.first_child;
1978 let mut seen = false;
1979 while let Some(NodeId(k)) = kid {
1980 if k == n.id.0 {
1981 seen = true;
1982 break;
1983 }
1984 kid = nodes[k as usize].next_sibling;
1985 }
1986 assert!(seen, "node {:?} not found among its parent's children", n.id);
1987 }
1988 }
1989
1990 #[test]
1991 fn editor_child_spans_and_subtree_agree_with_nodes() {
1992 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
1993 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
1994 let all = ed.nodes().expect("nodes");
1995 let doc = all.iter().find(|n| n.kind == "doc").expect("doc");
1996
1997 let top = ed.child_spans(None).expect("child_spans");
2000 let mut want = Vec::new();
2001 let mut c = doc.first_child;
2002 while let Some(id) = c {
2003 want.push(id);
2004 c = all[id.0 as usize].next_sibling;
2005 }
2006 assert_eq!(top.len(), want.len(), "top-level count");
2007 for (m, id) in top.iter().zip(&want) {
2008 assert_eq!(m.node_id, id.0, "child id");
2009 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
2010 assert_eq!(m.span, all[id.0 as usize].span, "child span");
2011 }
2012 assert!(src[top[0].span.clone()].starts_with('#'), "first block is the heading");
2014
2015 let list = top.iter().find(|m| m.kind.ends_with("list")).expect("a list");
2017 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
2018 assert_eq!(items.len(), 2);
2019 assert!(items.iter().all(|m| m.kind == "list_item"), "items: {items:?}");
2020
2021 let para = top.iter().find(|m| m.kind == "para").expect("a para").node_id;
2023 let sub = ed.subtree(NodeId(para)).expect("subtree");
2024 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
2025 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
2026 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
2027 assert_eq!(sub[0].kind, "para");
2028 for (i, n) in sub.iter().enumerate() {
2029 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
2030 for link in [n.parent, n.first_child, n.next_sibling].into_iter().flatten() {
2031 assert!((link.0 as usize) < sub.len(), "link {link:?} escapes the subtree");
2032 }
2033 }
2034 assert!(
2035 src[sub[0].span.clone()].starts_with("Hello"),
2036 "absolute span: {:?}",
2037 &src[sub[0].span.clone()]
2038 );
2039
2040 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<String> {
2042 let mut out = Vec::new();
2043 let mut stack = vec![root];
2044 while let Some(id) = stack.pop() {
2045 let n = &all[id.0 as usize];
2046 out.push(n.kind.clone());
2047 let mut c = n.first_child;
2048 while let Some(cid) = c {
2049 stack.push(cid);
2050 c = all[cid.0 as usize].next_sibling;
2051 }
2052 }
2053 out
2054 }
2055 let mut want_kinds = arena_kinds(&all, NodeId(para));
2056 let mut got_kinds: Vec<String> = sub.iter().map(|n| n.kind.clone()).collect();
2057 want_kinds.sort();
2058 got_kinds.sort();
2059 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
2060
2061 assert!(matches!(ed.subtree(NodeId(9999)), Err(Error::InvalidArgument)));
2063 }
2064
2065 #[test]
2066 fn flat_nodes_carry_table_head_and_alignment() {
2067 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
2071 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
2072 let nodes = ed.nodes().expect("nodes");
2073
2074 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == "row").collect();
2075 assert_eq!(rows.len(), 2, "a header row and one body row");
2076 assert_eq!(rows[0].head, Some(true), "first row is the header");
2077 assert_eq!(rows[1].head, Some(false), "second row is a body row");
2078
2079 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == "cell").collect();
2080 assert_eq!(cells.len(), 4);
2081 assert_eq!(cells[0].alignment, Some(Alignment::Left));
2083 assert_eq!(cells[1].alignment, Some(Alignment::Right));
2084 assert_eq!(cells[2].alignment, Some(Alignment::Left));
2085 assert_eq!(cells[3].alignment, Some(Alignment::Right));
2086 assert_eq!(cells[0].head, Some(true));
2088 assert_eq!(cells[2].head, Some(false));
2089
2090 let mut plain = Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
2093 let pnodes = plain.nodes().expect("nodes");
2094 let pcell = pnodes.iter().find(|n| n.kind == "cell").expect("a cell");
2095 assert_eq!(pcell.alignment, Some(Alignment::Default));
2096 }
2097
2098 #[test]
2099 fn editor_node_at_and_ancestors_hit_test_offsets() {
2100 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
2101
2102 let m = ed.node_at(2).expect("node_at").expect("a node covers offset 2");
2104 assert!(m.span.contains(&2));
2105
2106 let chain = ed.ancestors_at(2).expect("ancestors_at");
2108 assert!(!chain.is_empty());
2109 assert_eq!(chain[0].kind, "doc");
2110 assert_eq!(chain.last().unwrap().node_id, m.node_id);
2111
2112 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
2114 }
2115
2116 #[test]
2119 fn editor_wrap_and_toggle_inline_round_trip() {
2120 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
2121
2122 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
2124 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
2125 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
2126
2127 ed.toggle_inline(4, 8, InlineKind::Strong).expect("toggle off");
2129 assert_eq!(ed.source_str().unwrap(), "a word b\n");
2130
2131 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
2133 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
2134 }
2135
2136 #[test]
2137 fn editor_inline_kind_support_is_format_specific() {
2138 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
2140 assert_eq!(md.wrap_range(2, 6, InlineKind::Mark), Err(Error::UnsupportedFormat));
2141
2142 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
2144 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
2145 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
2146 }
2147
2148 #[test]
2149 fn editor_toggle_strips_verbatim_via_content_span() {
2150 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
2151 ed.toggle_inline(2, 8, InlineKind::Verbatim).expect("toggle code off");
2153 assert_eq!(ed.source_str().unwrap(), "a code b\n");
2154
2155 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
2158 ed2.toggle_inline(2, 7, InlineKind::Verbatim).expect("toggle multi off");
2159 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
2160 }
2161
2162 #[test]
2163 fn editor_set_block_switches_para_and_heading_levels() {
2164 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
2165
2166 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
2168 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
2169
2170 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
2172 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
2173
2174 ed.set_block(2, BlockKind::Paragraph).expect("to para");
2176 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
2177 }
2178
2179 #[test]
2180 fn editor_set_block_rejects_bad_level_and_format() {
2181 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2182 assert_eq!(md.set_block(0, BlockKind::Heading(9)), Err(Error::InvalidArgument));
2183
2184 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2185 assert_eq!(xml.set_block(1, BlockKind::Heading(1)), Err(Error::UnsupportedFormat));
2186 }
2187
2188 #[test]
2189 fn editor_toggle_block_container_round_trips() {
2190 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
2191
2192 let c = ed
2193 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
2194 .expect("quote on");
2195 assert_eq!(ed.source_str().unwrap(), "> a\n");
2196 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
2197
2198 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2199 .expect("quote off");
2200 assert_eq!(ed.source_str().unwrap(), "a\n");
2201 }
2202
2203 #[test]
2204 fn editor_toggle_block_container_nests_a_partial_selection() {
2205 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
2206
2207 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2210 .expect("nest");
2211 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
2212
2213 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
2215 .expect("peel");
2216 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
2217 }
2218
2219 #[test]
2220 fn editor_toggle_block_container_numbers_and_converts_lists() {
2221 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
2222
2223 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
2225 .expect("ordered on");
2226 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
2227
2228 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
2230 .expect("convert");
2231 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
2232 }
2233
2234 #[test]
2235 fn editor_toggle_block_container_rejects_unspellable_format() {
2236 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2237 assert_eq!(
2238 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
2239 Err(Error::UnsupportedFormat)
2240 );
2241 }
2242
2243 #[test]
2244 fn editor_insert_link_wraps_and_repoints() {
2245 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
2246
2247 ed.insert_link(2, 6, "http://x.dev").expect("link");
2248 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
2249
2250 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
2252 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
2253 }
2254
2255 #[test]
2256 fn editor_insert_link_repoints_an_autolink() {
2257 for format in [Format::Markdown, Format::Djot] {
2262 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
2263 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
2264 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
2265
2266 let nodes = ed.nodes().expect("nodes");
2268 let url = nodes
2269 .iter()
2270 .find(|n| n.kind == "url")
2271 .expect("still an autolink");
2272 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
2273 assert!(!nodes.iter().any(|n| n.kind == "link"));
2274 }
2275 }
2276
2277 #[test]
2278 fn editor_insert_link_escapes_the_destination() {
2279 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
2282 dj.insert_link(0, 1, "a)b").expect("link");
2283 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
2284
2285 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
2289 md.insert_link(0, 1, "a b").expect("link");
2290 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
2291
2292 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
2293 dj2.insert_link(0, 1, "a b").expect("link");
2294 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
2295 }
2296
2297 #[test]
2298 fn editor_insert_link_rejects_a_newline_destination() {
2299 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
2300 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
2301
2302 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2303 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
2304 }
2305
2306 #[test]
2307 fn editor_insert_literal_keeps_typed_specials_literal() {
2308 for format in [Format::Markdown, Format::Djot] {
2309 let mut ed = Editor::new_str("z\n", format).expect("editor");
2310 ed.insert_literal(0, "*hi*").expect("literal");
2312
2313 let nodes = ed.nodes().expect("nodes");
2315 assert!(!nodes.iter().any(|n| n.kind == "emph" || n.kind == "strong"));
2316 let text: String = nodes
2317 .iter()
2318 .filter(|n| n.kind == "str")
2319 .filter_map(|n| n.text.clone())
2320 .collect();
2321 assert_eq!(text, "*hi*z");
2322 }
2323 }
2324
2325 #[test]
2326 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
2327 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
2329 ed.insert_literal(1, "# ").expect("literal");
2330 assert_eq!(ed.source_str().unwrap(), "a# z\n");
2331
2332 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
2334 ed2.insert_literal(0, "# ").expect("literal");
2335 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
2336 assert!(!ed2.nodes().expect("nodes").iter().any(|n| n.kind == "heading"));
2337 }
2338
2339 #[test]
2340 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
2341 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
2342 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
2343
2344 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2345 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
2346 }
2347
2348 #[test]
2349 fn editor_insert_line_break_splices_in_cell_br() {
2350 let mut ed =
2351 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
2352 ed.insert_line_break(3).expect("line break");
2354 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
2355 let nodes = ed.nodes().expect("nodes");
2357 assert!(nodes.iter().any(|n| n.kind == "hard_break"));
2358 assert!(!nodes.iter().any(|n| n.kind == "raw_inline"));
2359 }
2360
2361 #[test]
2362 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
2363 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
2365 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
2366
2367 let mut dj =
2369 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
2370 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
2371
2372 let mut ed =
2374 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
2375 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
2376 }
2377
2378 #[test]
2379 fn editor_undo_redo_round_trip() {
2380 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2381 ed.edit_range(5, 5, "!").expect("edit");
2382 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2383
2384 let change = ed.undo().expect("undo ok").expect("something to undo");
2385 assert_eq!(ed.source_str().unwrap(), "hello\n");
2386 assert_eq!(change.new.end, 5);
2387 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
2388
2389 ed.redo().expect("redo ok").expect("something to redo");
2390 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2391 }
2392
2393 #[test]
2394 fn editor_coalesce_folds_a_run() {
2395 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2396 ed.edit_range(0, 0, "a").expect("edit");
2397 ed.edit_range(1, 1, "b").expect("edit");
2398 ed.coalesce_last_undo().expect("coalesce");
2399 assert_eq!(ed.source_str().unwrap(), "ab\n");
2400 ed.undo().expect("undo ok").expect("something to undo");
2402 assert_eq!(ed.source_str().unwrap(), "\n");
2403 assert!(ed.undo().expect("undo ok").is_none());
2404 }
2405
2406 #[test]
2407 fn editor_revision_bumps_per_successful_mutation() {
2408 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
2409 assert_eq!(ed.revision(), 0);
2410 ed.edit_range(1, 1, "y").expect("edit");
2411 assert_eq!(ed.revision(), 1);
2412
2413 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
2415 assert_eq!(xml.revision(), 0);
2416 assert!(xml.replace_content("0", "<b>").is_err());
2417 assert_eq!(xml.revision(), 0);
2418
2419 ed.undo().expect("undo ok").expect("something to undo");
2421 assert_eq!(ed.revision(), 2);
2422 ed.redo().expect("redo ok").expect("something to redo");
2423 assert_eq!(ed.revision(), 3);
2424 }
2425
2426 #[test]
2427 fn editor_dirty_range_tracks_and_clears() {
2428 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
2429 assert_eq!(ed.dirty_range(), None);
2431
2432 ed.edit_range(2, 2, "XY").expect("edit");
2434 assert_eq!(ed.dirty_range(), Some(2..4));
2435
2436 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
2440 assert!(d.start <= 2 && d.end >= 10, "range {d:?} must cover both edits");
2441
2442 let rev = ed.revision();
2444 ed.clear_dirty();
2445 assert_eq!(ed.dirty_range(), None);
2446 assert_eq!(ed.revision(), rev);
2447
2448 ed.undo().expect("undo ok").expect("something to undo");
2450 assert!(ed.dirty_range().is_some());
2451 }
2452
2453 #[test]
2454 fn editor_caret_blob_follows_undo_and_redo() {
2455 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2456 assert!(ed.caret_blob().unwrap().is_empty());
2457
2458 ed.set_caret_blob(b"before").expect("set caret");
2460 ed.edit_range(5, 5, "!").expect("edit");
2461 assert!(ed.caret_blob().unwrap().is_empty());
2463 ed.set_caret_blob(b"after").expect("set caret");
2464
2465 ed.undo().expect("undo ok").expect("something to undo");
2467 assert_eq!(ed.source_str().unwrap(), "hello\n");
2468 assert_eq!(ed.caret_blob().unwrap(), b"before");
2469
2470 ed.redo().expect("redo ok").expect("something to redo");
2472 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2473 assert_eq!(ed.caret_blob().unwrap(), b"after");
2474 }
2475
2476 #[test]
2477 fn editor_coalesced_run_keeps_the_pre_run_caret() {
2478 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2479 ed.set_caret_blob(b"c0").expect("set caret");
2480 ed.edit_range(0, 0, "a").expect("edit");
2481 ed.set_caret_blob(b"c1").expect("set caret");
2482 ed.edit_range(1, 1, "b").expect("edit");
2483 ed.coalesce_last_undo().expect("coalesce");
2484 ed.set_caret_blob(b"c2").expect("set caret");
2485
2486 ed.undo().expect("undo ok").expect("something to undo");
2488 assert_eq!(ed.source_str().unwrap(), "\n");
2489 assert_eq!(ed.caret_blob().unwrap(), b"c0");
2490 }
2491
2492 #[test]
2493 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
2494 let mut ed =
2495 Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
2496 ed.renumber_ordered_lists(0).expect("renumber ok");
2497 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
2498 }
2499
2500 #[test]
2501 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
2502 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
2503 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
2504 }
2505
2506 #[test]
2507 fn editor_table_insert_row_and_set_alignment() {
2508 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
2509 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
2510 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
2512 ed.source_str().unwrap(),
2513 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
2514 );
2515 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
2517 }
2518
2519 #[test]
2520 fn editor_table_edit_off_a_table_is_not_found() {
2521 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
2522 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
2523 }
2524
2525 #[test]
2526 fn editor_set_block_converts_setext_heading() {
2527 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
2529 ed.set_block(0, BlockKind::Heading(1)).expect("setext to atx");
2530 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
2531 }
2532
2533 #[test]
2534 fn editor_unwrap_and_smart_delete() {
2535 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
2536 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
2538
2539 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
2540 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
2542 }
2543
2544 #[test]
2545 fn editor_directives_require_the_extension_flag() {
2546 let src = ":::vis{.public}\nhi\n:::\n";
2547 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
2550 assert_eq!(plain.query("directive").expect("query").len(), 0);
2551 let mut ext = Editor::new_ext(
2553 src.as_bytes(),
2554 Format::Markdown,
2555 MarkdownExtensions { directives: true, ..Default::default() },
2556 )
2557 .expect("editor");
2558 assert_eq!(ext.query("directive").expect("query").len(), 1);
2559 }
2560
2561 #[test]
2562 fn document_html_elements_make_embedded_img_queryable() {
2563 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
2564 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
2566 assert_eq!(plain.query("image").expect("query").len(), 0);
2567 let mut ext = Document::parse_str_with(
2569 src,
2570 Format::Markdown,
2571 MarkdownExtensions { html_elements: true, ..Default::default() },
2572 )
2573 .expect("parse");
2574 let images = ext.query("image").expect("query");
2575 assert_eq!(images.len(), 1);
2576 assert_eq!(images[0].kind, "image");
2577 }
2578
2579 #[test]
2580 fn editor_filter_public_audience_view() {
2581 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
2582 let mut ed = Editor::new_ext(
2583 src.as_bytes(),
2584 Format::Markdown,
2585 MarkdownExtensions { directives: true, ..Default::default() },
2586 )
2587 .expect("editor");
2588 ed.filter(
2590 "directive[name=vis]",
2591 Some("directive[class~=public]"),
2592 true,
2593 )
2594 .expect("filter");
2595 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
2596 }
2597
2598 #[test]
2599 fn editor_filter_rejects_a_malformed_selector() {
2600 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2601 assert_eq!(ed.filter("list >", None, false), Err(Error::InvalidArgument));
2602 }
2603
2604 #[test]
2605 fn builder_builds_and_renders_a_document() {
2606 let mut b = Builder::new().expect("builder");
2607
2608 let title = b.add_text(TextKind::Str, "Title").unwrap();
2610 let heading = b.add_heading(1).unwrap();
2611 b.set_children(heading, &[title]).unwrap();
2612
2613 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
2614 let world = b.add_text(TextKind::Str, "world").unwrap();
2615 let emph = b.add(VoidKind::Emph).unwrap();
2616 b.set_children(emph, &[world]).unwrap();
2617 let para = b.add(VoidKind::Para).unwrap();
2618 b.set_children(para, &[hello, emph]).unwrap();
2619
2620 let doc = b.add(VoidKind::Doc).unwrap();
2621 b.set_children(doc, &[heading, para]).unwrap();
2622
2623 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
2624 assert!(html.contains("<h1>Title</h1>"), "{html}");
2625 assert!(html.contains("<em>world</em>"), "{html}");
2626
2627 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
2628 assert!(md.contains("# Title"), "{md}");
2629 assert!(md.contains("*world*"), "{md}");
2630
2631 let matches = b.query(doc, "heading").unwrap();
2632 assert_eq!(matches.len(), 1);
2633 assert_eq!(matches[0].kind, "heading");
2634
2635 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
2636 assert!(json.contains("\"kind\": \"doc\""), "{json}");
2637 }
2638
2639 #[test]
2640 fn builder_element_with_attributes() {
2641 let mut b = Builder::new().expect("builder");
2642 let inner = b.add_text(TextKind::Str, "hi").unwrap();
2643 let el = b.add_element("section").unwrap();
2644 b.set_children(el, &[inner]).unwrap();
2645 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)]).unwrap();
2646
2647 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
2648 assert!(html.contains("<section"), "{html}");
2649 assert!(html.contains("class=\"note\""), "{html}");
2650 assert!(html.contains("hidden"), "{html}");
2651 }
2652
2653 #[test]
2654 fn builder_lists_round_trip_to_markdown() {
2655 let mut b = Builder::new().expect("builder");
2656
2657 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
2659 let one_para = b.add(VoidKind::Para).unwrap();
2660 b.set_children(one_para, &[one_txt]).unwrap();
2661 let one = b.add(VoidKind::ListItem).unwrap();
2662 b.set_children(one, &[one_para]).unwrap();
2663
2664 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
2665 let two_para = b.add(VoidKind::Para).unwrap();
2666 b.set_children(two_para, &[two_txt]).unwrap();
2667 let two = b.add(VoidKind::ListItem).unwrap();
2668 b.set_children(two, &[two_para]).unwrap();
2669
2670 let list = b
2671 .add_ordered_list(OrderedNumbering::Decimal, OrderedDelim::Period, true, Some(1))
2672 .unwrap();
2673 b.set_children(list, &[one, two]).unwrap();
2674 let doc = b.add(VoidKind::Doc).unwrap();
2675 b.set_children(doc, &[list]).unwrap();
2676
2677 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
2678 assert!(md.contains("1. one"), "{md}");
2679 assert!(md.contains("2. two"), "{md}");
2680 }
2681
2682 #[test]
2683 fn builder_rejects_invalid_kind_and_id() {
2684 let b = Builder::new().expect("builder");
2685 let mut id = 0u32;
2689 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
2690 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2691
2692 let mut ptr = std::ptr::null();
2694 let mut len = 0usize;
2695 let status = unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
2696 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2697 }
2698}