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_image(
1015 &mut self,
1016 start: usize,
1017 end: usize,
1018 destination: &str,
1019 ) -> Result<Change, Error> {
1020 self.change_op(|ed, out| unsafe {
1021 ffi::twig_editor_insert_image(
1022 ed,
1023 start,
1024 end,
1025 destination.as_ptr(),
1026 destination.len(),
1027 out,
1028 )
1029 })
1030 }
1031
1032 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
1053 self.change_op(|ed, out| unsafe {
1054 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
1055 })
1056 }
1057
1058 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
1072 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
1073 }
1074
1075 fn change_op(
1078 &mut self,
1079 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
1080 ) -> Result<Change, Error> {
1081 let mut change = ffi::TwigChange {
1082 old_span: ffi::TwigSpan { start: 0, end: 0 },
1083 new_span: ffi::TwigSpan { start: 0, end: 0 },
1084 };
1085 let status = op(self.raw.as_ptr(), &mut change);
1086 Error::from_status(status)?;
1087 Ok(Change::from_ffi(change))
1088 }
1089
1090 fn apply(
1092 &mut self,
1093 locator: &str,
1094 text: &str,
1095 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
1096 ) -> Result<(), Error> {
1097 let status = op(
1098 self.raw.as_ptr(),
1099 locator.as_ptr(),
1100 locator.len(),
1101 text.as_ptr(),
1102 text.len(),
1103 );
1104 Error::from_status(status)
1105 }
1106}
1107
1108impl Drop for Editor {
1109 fn drop(&mut self) {
1110 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
1111 }
1112}
1113
1114fn collect_bytes(
1119 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
1120) -> Result<Vec<u8>, Error> {
1121 let mut ptr = std::ptr::null();
1122 let mut len = 0usize;
1123 let status = call(&mut ptr, &mut len);
1124 Error::from_status(status)?;
1125 if len == 0 {
1126 return Ok(Vec::new());
1127 }
1128 if ptr.is_null() {
1129 return Err(Error::Internal);
1130 }
1131 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1132 Ok(bytes.to_vec())
1133}
1134
1135fn collect_matches(
1138 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
1139) -> Result<Vec<QueryMatch>, Error> {
1140 let mut ptr = std::ptr::null();
1141 let mut len = 0usize;
1142 let status = call(&mut ptr, &mut len);
1143 Error::from_status(status)?;
1144 if len == 0 {
1145 return Ok(Vec::new());
1146 }
1147 if ptr.is_null() {
1148 return Err(Error::Internal);
1149 }
1150 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1151 matches.iter().map(query_match_from_ffi).collect()
1152}
1153
1154fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
1157 Ok(QueryMatch {
1158 node_id: m.node_id,
1159 span: m.span.start..m.span.end,
1160 content_span: if m.has_content_span != 0 {
1161 Some(m.content_span.start..m.content_span.end)
1162 } else {
1163 None
1164 },
1165 kind: borrowed_cstr(m.kind)?,
1166 })
1167}
1168
1169fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
1171 let node_id = |v: u32| if v == ffi::TWIG_NO_NODE { None } else { Some(NodeId(v)) };
1172 Ok(FlatNode {
1173 id: NodeId(n.id),
1174 parent: node_id(n.parent),
1175 first_child: node_id(n.first_child),
1176 next_sibling: node_id(n.next_sibling),
1177 span: n.span.start..n.span.end,
1178 content_span: if n.has_content_span != 0 {
1179 Some(n.content_span.start..n.content_span.end)
1180 } else {
1181 None
1182 },
1183 level: if n.level != 0 { Some(n.level) } else { None },
1184 kind: borrowed_cstr(n.kind)?,
1185 text: borrowed_bytes(n.text_ptr, n.text_len),
1186 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
1187 head: match n.head {
1188 ffi::TWIG_HEAD_NONE => None,
1189 v => Some(v != 0),
1190 },
1191 alignment: Alignment::from_c(n.alignment),
1192 name: borrowed_bytes(n.name_ptr, n.name_len),
1193 directive_form: DirectiveForm::from_c(n.directive_form),
1194 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
1195 })
1196}
1197
1198fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
1202 if ptr.is_null() || len == 0 {
1203 return Vec::new();
1204 }
1205 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
1206 kvs.iter()
1207 .map(|kv| {
1208 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
1209 (key, borrowed_bytes(kv.value, kv.value_len))
1210 })
1211 .collect()
1212}
1213
1214fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
1216 if ptr.is_null() {
1217 return Err(Error::Internal);
1218 }
1219 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
1220 .to_str()
1221 .map_err(|_| Error::Internal)?
1222 .to_owned())
1223}
1224
1225fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
1229 if ptr.is_null() {
1230 return None;
1231 }
1232 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1233 Some(String::from_utf8_lossy(bytes).into_owned())
1234}
1235
1236#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1240pub struct NodeId(pub u32);
1241
1242#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1245pub enum VoidKind {
1246 Doc,
1247 Para,
1248 ThematicBreak,
1249 Section,
1250 Div,
1251 BlockQuote,
1252 DefinitionList,
1253 Table,
1254 ListItem,
1255 DefinitionListItem,
1256 Term,
1257 Definition,
1258 Caption,
1259 SoftBreak,
1260 HardBreak,
1261 NonBreakingSpace,
1262 Emph,
1263 Strong,
1264 Span,
1265 Mark,
1266 Superscript,
1267 Subscript,
1268 Insert,
1269 Delete,
1270 DoubleQuoted,
1271 SingleQuoted,
1272}
1273
1274impl VoidKind {
1275 fn to_c(self) -> c_int {
1276 match self {
1278 VoidKind::Doc => 0,
1279 VoidKind::Para => 1,
1280 VoidKind::ThematicBreak => 3,
1281 VoidKind::Section => 4,
1282 VoidKind::Div => 5,
1283 VoidKind::BlockQuote => 9,
1284 VoidKind::DefinitionList => 13,
1285 VoidKind::Table => 14,
1286 VoidKind::ListItem => 15,
1287 VoidKind::DefinitionListItem => 17,
1288 VoidKind::Term => 18,
1289 VoidKind::Definition => 19,
1290 VoidKind::Caption => 22,
1291 VoidKind::SoftBreak => 26,
1292 VoidKind::HardBreak => 27,
1293 VoidKind::NonBreakingSpace => 28,
1294 VoidKind::Emph => 38,
1295 VoidKind::Strong => 39,
1296 VoidKind::Span => 42,
1297 VoidKind::Mark => 43,
1298 VoidKind::Superscript => 44,
1299 VoidKind::Subscript => 45,
1300 VoidKind::Insert => 46,
1301 VoidKind::Delete => 47,
1302 VoidKind::DoubleQuoted => 48,
1303 VoidKind::SingleQuoted => 49,
1304 }
1305 }
1306}
1307
1308#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1310pub enum TextKind {
1311 Str,
1312 Symb,
1313 Verbatim,
1314 InlineMath,
1315 DisplayMath,
1316 Url,
1317 Email,
1318 FootnoteReference,
1319 Comment,
1320 Doctype,
1321 Cdata,
1322}
1323
1324impl TextKind {
1325 fn to_c(self) -> c_int {
1326 match self {
1327 TextKind::Str => 25,
1328 TextKind::Symb => 29,
1329 TextKind::Verbatim => 30,
1330 TextKind::InlineMath => 32,
1331 TextKind::DisplayMath => 33,
1332 TextKind::Url => 34,
1333 TextKind::Email => 35,
1334 TextKind::FootnoteReference => 36,
1335 TextKind::Comment => 52,
1336 TextKind::Doctype => 53,
1337 TextKind::Cdata => 55,
1338 }
1339 }
1340}
1341
1342#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1344pub enum BulletStyle {
1345 Dash,
1346 Plus,
1347 Star,
1348}
1349
1350impl BulletStyle {
1351 fn to_c(self) -> c_int {
1352 match self {
1353 BulletStyle::Dash => 0,
1354 BulletStyle::Plus => 1,
1355 BulletStyle::Star => 2,
1356 }
1357 }
1358}
1359
1360#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1362pub enum OrderedNumbering {
1363 Decimal,
1364 LowerAlpha,
1365 UpperAlpha,
1366 LowerRoman,
1367 UpperRoman,
1368}
1369
1370impl OrderedNumbering {
1371 fn to_c(self) -> c_int {
1372 match self {
1373 OrderedNumbering::Decimal => 0,
1374 OrderedNumbering::LowerAlpha => 1,
1375 OrderedNumbering::UpperAlpha => 2,
1376 OrderedNumbering::LowerRoman => 3,
1377 OrderedNumbering::UpperRoman => 4,
1378 }
1379 }
1380}
1381
1382#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1384pub enum OrderedDelim {
1385 Period,
1386 ParenAfter,
1387 ParenBoth,
1388}
1389
1390impl OrderedDelim {
1391 fn to_c(self) -> c_int {
1392 match self {
1393 OrderedDelim::Period => 0,
1394 OrderedDelim::ParenAfter => 1,
1395 OrderedDelim::ParenBoth => 2,
1396 }
1397 }
1398}
1399
1400#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1403pub enum Alignment {
1404 Default,
1405 Left,
1406 Right,
1407 Center,
1408}
1409
1410impl Alignment {
1411 fn to_c(self) -> c_int {
1412 match self {
1413 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
1414 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
1415 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
1416 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
1417 }
1418 }
1419
1420 fn from_c(v: c_int) -> Option<Self> {
1423 match v {
1424 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
1425 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
1426 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
1427 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
1428 _ => None,
1429 }
1430 }
1431}
1432
1433#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1435pub enum SmartPunctuation {
1436 LeftSingleQuote,
1437 RightSingleQuote,
1438 LeftDoubleQuote,
1439 RightDoubleQuote,
1440 Ellipses,
1441 EmDash,
1442 EnDash,
1443}
1444
1445impl SmartPunctuation {
1446 fn to_c(self) -> c_int {
1447 match self {
1448 SmartPunctuation::LeftSingleQuote => 0,
1449 SmartPunctuation::RightSingleQuote => 1,
1450 SmartPunctuation::LeftDoubleQuote => 2,
1451 SmartPunctuation::RightDoubleQuote => 3,
1452 SmartPunctuation::Ellipses => 4,
1453 SmartPunctuation::EmDash => 5,
1454 SmartPunctuation::EnDash => 6,
1455 }
1456 }
1457}
1458
1459#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1461pub enum DirectiveForm {
1462 Text,
1463 Leaf,
1464 Container,
1465}
1466
1467impl DirectiveForm {
1468 fn to_c(self) -> c_int {
1469 match self {
1470 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
1471 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
1472 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
1473 }
1474 }
1475
1476 fn from_c(v: c_int) -> Option<Self> {
1480 match v {
1481 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
1482 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
1483 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
1484 _ => None,
1485 }
1486 }
1487}
1488
1489fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
1493 match s {
1494 Some(x) => (x.as_ptr(), x.len(), 1),
1495 None => (std::ptr::null(), 0, 0),
1496 }
1497}
1498
1499#[derive(Debug)]
1506pub struct Builder {
1507 raw: NonNull<ffi::TwigBuilder>,
1508}
1509
1510impl Builder {
1511 pub fn new() -> Result<Self, Error> {
1513 let mut raw = std::ptr::null_mut();
1514 let status = unsafe { ffi::twig_builder_create(&mut raw) };
1515 Error::from_status(status)?;
1516 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1517 Ok(Self { raw })
1518 }
1519
1520 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
1523 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
1524 }
1525
1526 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
1528 self.emit(|b, out| unsafe { ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out) })
1529 }
1530
1531 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
1533 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
1534 }
1535
1536 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
1538 let (lp, ll, has) = opt_str(lang);
1539 self.emit(|b, out| unsafe { ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out) })
1540 }
1541
1542 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1544 self.emit(|b, out| unsafe {
1545 ffi::twig_builder_add_raw_block(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1546 })
1547 }
1548
1549 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
1551 self.emit(|b, out| unsafe {
1552 ffi::twig_builder_add_metadata(b, lang.as_ptr(), lang.len(), text.as_ptr(), text.len(), out)
1553 })
1554 }
1555
1556 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1558 self.emit(|b, out| unsafe {
1559 ffi::twig_builder_add_raw_inline(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1560 })
1561 }
1562
1563 pub fn add_smart_punctuation(&mut self, kind: SmartPunctuation, text: &str) -> Result<NodeId, Error> {
1565 self.emit(|b, out| unsafe {
1566 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
1567 })
1568 }
1569
1570 pub fn add_link(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1573 let (dp, dl, hd) = opt_str(destination);
1574 let (rp, rl, hr) = opt_str(reference);
1575 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
1576 }
1577
1578 pub fn add_image(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1580 let (dp, dl, hd) = opt_str(destination);
1581 let (rp, rl, hr) = opt_str(reference);
1582 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
1583 }
1584
1585 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
1587 self.emit(|b, out| unsafe { ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out) })
1588 }
1589
1590 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
1592 self.emit(|b, out| unsafe { ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out) })
1593 }
1594
1595 pub fn add_processing_instruction(&mut self, target: &str, data: &str) -> Result<NodeId, Error> {
1597 self.emit(|b, out| unsafe {
1598 ffi::twig_builder_add_processing_instruction(b, target.as_ptr(), target.len(), data.as_ptr(), data.len(), out)
1599 })
1600 }
1601
1602 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
1604 self.emit(|b, out| unsafe { ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out) })
1605 }
1606
1607 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
1609 self.emit(|b, out| unsafe {
1610 ffi::twig_builder_add_reference(b, label.as_ptr(), label.len(), destination.as_ptr(), destination.len(), out)
1611 })
1612 }
1613
1614 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
1616 self.emit(|b, out| unsafe { ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out) })
1617 }
1618
1619 pub fn add_ordered_list(
1621 &mut self,
1622 numbering: OrderedNumbering,
1623 delim: OrderedDelim,
1624 tight: bool,
1625 start: Option<u32>,
1626 ) -> Result<NodeId, Error> {
1627 let (start_val, has_start) = match start {
1628 Some(s) => (s, 1),
1629 None => (0, 0),
1630 };
1631 self.emit(|b, out| unsafe {
1632 ffi::twig_builder_add_ordered_list(b, numbering.to_c(), delim.to_c(), tight as c_int, start_val, has_start, out)
1633 })
1634 }
1635
1636 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
1638 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
1639 }
1640
1641 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
1643 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list_item(b, checked as c_int, out) })
1644 }
1645
1646 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
1648 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
1649 }
1650
1651 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
1653 self.emit(|b, out| unsafe { ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out) })
1654 }
1655
1656 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
1659 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
1660 let status = unsafe { ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len()) };
1661 Error::from_status(status)
1662 }
1663
1664 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
1668 let kvs: Vec<ffi::TwigKeyVal> = attrs
1669 .iter()
1670 .map(|(k, v)| ffi::TwigKeyVal {
1671 key: k.as_ptr(),
1672 key_len: k.len(),
1673 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
1674 value_len: v.map_or(0, |s| s.len()),
1675 })
1676 .collect();
1677 let status = unsafe { ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len()) };
1678 Error::from_status(status)
1679 }
1680
1681 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1684 let raw = self.raw.as_ptr();
1685 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
1686 }
1687
1688 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
1692 let raw = self.raw.as_ptr();
1693 let ffi_format: ffi::TwigFormat = format.into();
1694 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_serialize(raw, root.0, ffi_format as i32, ptr, len) })
1695 }
1696
1697 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1699 let raw = self.raw.as_ptr();
1700 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
1701 }
1702
1703 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1706 let raw = self.raw.as_ptr();
1707 collect_matches(|ptr, len| unsafe {
1708 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
1709 })
1710 }
1711
1712 fn emit(
1715 &mut self,
1716 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
1717 ) -> Result<NodeId, Error> {
1718 let mut id: u32 = 0;
1719 let status = call(self.raw.as_ptr(), &mut id);
1720 Error::from_status(status)?;
1721 Ok(NodeId(id))
1722 }
1723}
1724
1725impl Drop for Builder {
1726 fn drop(&mut self) {
1727 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
1728 }
1729}
1730
1731#[cfg(test)]
1732mod tests {
1733 use super::*;
1734
1735 #[test]
1736 fn abi_version_matches() {
1737 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
1741 }
1742
1743 #[test]
1744 fn parses_and_renders_markdown_html() {
1745 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1746 let html = doc.render_html().expect("render html");
1747 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
1748 }
1749
1750 #[test]
1751 fn parses_html_input() {
1752 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
1753 let html = doc.render_html().expect("render html");
1754 assert!(String::from_utf8_lossy(&html).contains("hi"));
1755 }
1756
1757 #[test]
1758 fn serialize_round_trips_and_cross_converts() {
1759 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1760
1761 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
1762 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
1763
1764 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
1766 }
1767
1768 #[test]
1769 fn serialize_markdown_to_djot() {
1770 let mut doc =
1771 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
1772 let djot = doc.serialize(Format::Djot).expect("serialize djot");
1773 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
1774 }
1775
1776 #[test]
1777 fn ast_json_dumps_the_tree() {
1778 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
1779 let json = doc.ast_json().expect("ast json");
1780 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1781 }
1782
1783 #[test]
1784 fn query_finds_nodes_by_selector() {
1785 let source = "# One\n\n## Two\n";
1786 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1787 let matches = doc.query("heading").expect("query");
1788
1789 assert_eq!(matches.len(), 2);
1790 for m in &matches {
1791 assert_eq!(m.kind, "heading");
1792 assert!(m.span.start < m.span.end);
1793 }
1794 }
1795
1796 #[test]
1797 fn query_recovers_code_spans() {
1798 let source = "prose `code` more prose\n";
1799 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1800 let matches = doc.query("verbatim").expect("query");
1801
1802 assert_eq!(matches.len(), 1);
1803 assert_eq!(&source[matches[0].span.clone()], "`code`");
1804 }
1805
1806 #[test]
1807 fn query_rejects_a_malformed_selector() {
1808 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
1809 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
1810 }
1811
1812 #[test]
1813 fn editor_edits_by_index_path() {
1814 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
1815 ed.replace_content("0.0", "bye").expect("replace_content");
1816 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
1817 }
1818
1819 #[test]
1820 fn flat_nodes_expose_element_name_and_attrs() {
1821 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
1825 let mut ed = Editor::new_ext(
1826 src.as_bytes(),
1827 Format::Markdown,
1828 MarkdownExtensions { html_elements: true, ..Default::default() },
1829 )
1830 .expect("editor");
1831 let nodes = ed.nodes().expect("nodes");
1832
1833 let source = nodes
1834 .iter()
1835 .find(|n| n.name.as_deref() == Some("source"))
1836 .expect("a <source> element node");
1837 assert_eq!(
1838 source.attrs,
1839 vec![
1840 ("media".to_string(), Some("(prefers-color-scheme: dark)".to_string())),
1841 ("srcset".to_string(), Some("d.svg".to_string())),
1842 ]
1843 );
1844
1845 let img = nodes.iter().find(|n| n.kind == "image").expect("an image node");
1848 assert!(img.name.is_none());
1849 assert_eq!(img.destination.as_deref(), Some("l.svg"));
1850
1851 let picture_kids_str = nodes.iter().find(|n| n.kind == "str");
1853 if let Some(s) = picture_kids_str {
1854 assert!(s.name.is_none() && s.attrs.is_empty());
1855 }
1856 }
1857
1858 #[test]
1859 fn flat_nodes_expose_directive_name_and_form() {
1860 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
1866 let mut ed = Editor::new_ext(
1867 src.as_bytes(),
1868 Format::Markdown,
1869 MarkdownExtensions { directives: true, ..Default::default() },
1870 )
1871 .expect("editor");
1872 let nodes = ed.nodes().expect("nodes");
1873
1874 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
1875 .iter()
1876 .filter(|n| n.kind == "directive")
1877 .map(|n| (n.name.as_deref(), n.directive_form))
1878 .collect();
1879 assert_eq!(
1880 forms,
1881 vec![
1882 (Some("note"), Some(DirectiveForm::Container)),
1883 (Some("embed"), Some(DirectiveForm::Leaf)),
1884 (Some("abbr"), Some(DirectiveForm::Text)),
1885 ]
1886 );
1887
1888 let embed = nodes.iter().find(|n| n.name.as_deref() == Some("embed")).expect("embed");
1891 assert_eq!(embed.attrs, vec![("src".to_string(), Some("demo.html".to_string()))]);
1892 let para = nodes.iter().find(|n| n.kind == "para").expect("a para");
1893 assert!(para.directive_form.is_none() && para.name.is_none());
1894 }
1895
1896 #[test]
1897 fn editor_insert_child_and_delete() {
1898 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
1899 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1900 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
1901 ed.delete("0.1").expect("delete");
1902 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
1903 }
1904
1905 #[test]
1906 fn editor_edits_by_selector() {
1907 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1908 ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1909 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
1910 }
1911
1912 #[test]
1913 fn editor_locator_errors_are_distinct() {
1914 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
1915 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
1916 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
1917 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
1918 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
1920 }
1921
1922 #[test]
1923 fn editor_reparse_break_rolls_back() {
1924 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
1925 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
1926 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
1927 }
1928
1929 #[test]
1930 fn editor_leaf_content_is_not_editable() {
1931 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1932 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
1933 }
1934
1935 #[test]
1936 fn editor_query_reflects_current_tree() {
1937 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
1938 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1939 assert_eq!(ed.query("element").expect("query").len(), 3);
1941 let json = ed.ast_json().expect("ast_json");
1942 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1943 }
1944
1945 #[test]
1948 fn editor_edit_range_types_backspaces_and_reports_change() {
1949 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
1950
1951 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
1953 assert_eq!(ed.source_str().unwrap(), "aXb\n");
1954 assert_eq!(c.old, 1..1);
1955 assert_eq!(c.new, 1..2);
1956 assert_eq!(c.delta(), 1);
1957
1958 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
1960 assert_eq!(ed.source_str().unwrap(), "ab\n");
1961 assert_eq!(c2.old, 1..2);
1962 assert_eq!(c2.new, 1..1);
1963 assert_eq!(c2.delta(), -1);
1964 }
1965
1966 #[test]
1967 fn editor_edit_range_rejects_bad_ranges() {
1968 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1969 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"); }
1973
1974 #[test]
1975 fn editor_last_change_reports_locator_ops_too() {
1976 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1977 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1980 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
1981 let c = ed.last_change().expect("a change was recorded");
1982 assert_eq!(c.old, 7..13);
1984 assert_eq!(c.new, 7..17);
1985 }
1986
1987 #[test]
1988 fn editor_nodes_is_a_walkable_flat_tree() {
1989 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1990 let nodes = ed.nodes().expect("nodes");
1991 assert!(!nodes.is_empty());
1992
1993 for (i, n) in nodes.iter().enumerate() {
1995 assert_eq!(n.id, NodeId(i as u32));
1996 }
1997 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
1999 assert_eq!(roots.len(), 1);
2000 assert_eq!(roots[0].kind, "doc");
2001
2002 let heading = nodes.iter().find(|n| n.kind == "heading").expect("a heading");
2004 assert_eq!(heading.level, Some(1));
2005 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
2006
2007 assert_eq!(heading.head, None);
2009 assert_eq!(heading.alignment, None);
2010
2011 for n in nodes.iter().filter(|n| n.parent.is_some()) {
2014 let p = &nodes[n.parent.unwrap().0 as usize];
2015 let mut kid = p.first_child;
2016 let mut seen = false;
2017 while let Some(NodeId(k)) = kid {
2018 if k == n.id.0 {
2019 seen = true;
2020 break;
2021 }
2022 kid = nodes[k as usize].next_sibling;
2023 }
2024 assert!(seen, "node {:?} not found among its parent's children", n.id);
2025 }
2026 }
2027
2028 #[test]
2029 fn editor_child_spans_and_subtree_agree_with_nodes() {
2030 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
2031 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
2032 let all = ed.nodes().expect("nodes");
2033 let doc = all.iter().find(|n| n.kind == "doc").expect("doc");
2034
2035 let top = ed.child_spans(None).expect("child_spans");
2038 let mut want = Vec::new();
2039 let mut c = doc.first_child;
2040 while let Some(id) = c {
2041 want.push(id);
2042 c = all[id.0 as usize].next_sibling;
2043 }
2044 assert_eq!(top.len(), want.len(), "top-level count");
2045 for (m, id) in top.iter().zip(&want) {
2046 assert_eq!(m.node_id, id.0, "child id");
2047 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
2048 assert_eq!(m.span, all[id.0 as usize].span, "child span");
2049 }
2050 assert!(src[top[0].span.clone()].starts_with('#'), "first block is the heading");
2052
2053 let list = top.iter().find(|m| m.kind.ends_with("list")).expect("a list");
2055 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
2056 assert_eq!(items.len(), 2);
2057 assert!(items.iter().all(|m| m.kind == "list_item"), "items: {items:?}");
2058
2059 let para = top.iter().find(|m| m.kind == "para").expect("a para").node_id;
2061 let sub = ed.subtree(NodeId(para)).expect("subtree");
2062 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
2063 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
2064 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
2065 assert_eq!(sub[0].kind, "para");
2066 for (i, n) in sub.iter().enumerate() {
2067 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
2068 for link in [n.parent, n.first_child, n.next_sibling].into_iter().flatten() {
2069 assert!((link.0 as usize) < sub.len(), "link {link:?} escapes the subtree");
2070 }
2071 }
2072 assert!(
2073 src[sub[0].span.clone()].starts_with("Hello"),
2074 "absolute span: {:?}",
2075 &src[sub[0].span.clone()]
2076 );
2077
2078 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<String> {
2080 let mut out = Vec::new();
2081 let mut stack = vec![root];
2082 while let Some(id) = stack.pop() {
2083 let n = &all[id.0 as usize];
2084 out.push(n.kind.clone());
2085 let mut c = n.first_child;
2086 while let Some(cid) = c {
2087 stack.push(cid);
2088 c = all[cid.0 as usize].next_sibling;
2089 }
2090 }
2091 out
2092 }
2093 let mut want_kinds = arena_kinds(&all, NodeId(para));
2094 let mut got_kinds: Vec<String> = sub.iter().map(|n| n.kind.clone()).collect();
2095 want_kinds.sort();
2096 got_kinds.sort();
2097 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
2098
2099 assert!(matches!(ed.subtree(NodeId(9999)), Err(Error::InvalidArgument)));
2101 }
2102
2103 #[test]
2104 fn flat_nodes_carry_table_head_and_alignment() {
2105 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
2109 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
2110 let nodes = ed.nodes().expect("nodes");
2111
2112 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == "row").collect();
2113 assert_eq!(rows.len(), 2, "a header row and one body row");
2114 assert_eq!(rows[0].head, Some(true), "first row is the header");
2115 assert_eq!(rows[1].head, Some(false), "second row is a body row");
2116
2117 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == "cell").collect();
2118 assert_eq!(cells.len(), 4);
2119 assert_eq!(cells[0].alignment, Some(Alignment::Left));
2121 assert_eq!(cells[1].alignment, Some(Alignment::Right));
2122 assert_eq!(cells[2].alignment, Some(Alignment::Left));
2123 assert_eq!(cells[3].alignment, Some(Alignment::Right));
2124 assert_eq!(cells[0].head, Some(true));
2126 assert_eq!(cells[2].head, Some(false));
2127
2128 let mut plain = Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
2131 let pnodes = plain.nodes().expect("nodes");
2132 let pcell = pnodes.iter().find(|n| n.kind == "cell").expect("a cell");
2133 assert_eq!(pcell.alignment, Some(Alignment::Default));
2134 }
2135
2136 #[test]
2137 fn editor_node_at_and_ancestors_hit_test_offsets() {
2138 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
2139
2140 let m = ed.node_at(2).expect("node_at").expect("a node covers offset 2");
2142 assert!(m.span.contains(&2));
2143
2144 let chain = ed.ancestors_at(2).expect("ancestors_at");
2146 assert!(!chain.is_empty());
2147 assert_eq!(chain[0].kind, "doc");
2148 assert_eq!(chain.last().unwrap().node_id, m.node_id);
2149
2150 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
2152 }
2153
2154 #[test]
2157 fn editor_wrap_and_toggle_inline_round_trip() {
2158 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
2159
2160 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
2162 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
2163 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
2164
2165 ed.toggle_inline(4, 8, InlineKind::Strong).expect("toggle off");
2167 assert_eq!(ed.source_str().unwrap(), "a word b\n");
2168
2169 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
2171 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
2172 }
2173
2174 #[test]
2175 fn editor_inline_kind_support_is_format_specific() {
2176 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
2178 assert_eq!(md.wrap_range(2, 6, InlineKind::Mark), Err(Error::UnsupportedFormat));
2179
2180 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
2182 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
2183 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
2184 }
2185
2186 #[test]
2187 fn editor_toggle_strips_verbatim_via_content_span() {
2188 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
2189 ed.toggle_inline(2, 8, InlineKind::Verbatim).expect("toggle code off");
2191 assert_eq!(ed.source_str().unwrap(), "a code b\n");
2192
2193 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
2196 ed2.toggle_inline(2, 7, InlineKind::Verbatim).expect("toggle multi off");
2197 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
2198 }
2199
2200 #[test]
2201 fn editor_set_block_switches_para_and_heading_levels() {
2202 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
2203
2204 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
2206 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
2207
2208 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
2210 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
2211
2212 ed.set_block(2, BlockKind::Paragraph).expect("to para");
2214 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
2215 }
2216
2217 #[test]
2218 fn editor_set_block_rejects_bad_level_and_format() {
2219 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2220 assert_eq!(md.set_block(0, BlockKind::Heading(9)), Err(Error::InvalidArgument));
2221
2222 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2223 assert_eq!(xml.set_block(1, BlockKind::Heading(1)), Err(Error::UnsupportedFormat));
2224 }
2225
2226 #[test]
2227 fn editor_toggle_block_container_round_trips() {
2228 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
2229
2230 let c = ed
2231 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
2232 .expect("quote on");
2233 assert_eq!(ed.source_str().unwrap(), "> a\n");
2234 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
2235
2236 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2237 .expect("quote off");
2238 assert_eq!(ed.source_str().unwrap(), "a\n");
2239 }
2240
2241 #[test]
2242 fn editor_toggle_block_container_nests_a_partial_selection() {
2243 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
2244
2245 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2248 .expect("nest");
2249 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
2250
2251 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
2253 .expect("peel");
2254 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
2255 }
2256
2257 #[test]
2258 fn editor_toggle_block_container_numbers_and_converts_lists() {
2259 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
2260
2261 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
2263 .expect("ordered on");
2264 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
2265
2266 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
2268 .expect("convert");
2269 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
2270 }
2271
2272 #[test]
2273 fn editor_toggle_block_container_rejects_unspellable_format() {
2274 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2275 assert_eq!(
2276 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
2277 Err(Error::UnsupportedFormat)
2278 );
2279 }
2280
2281 #[test]
2282 fn editor_insert_link_wraps_and_repoints() {
2283 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
2284
2285 ed.insert_link(2, 6, "http://x.dev").expect("link");
2286 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
2287
2288 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
2290 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
2291 }
2292
2293 #[test]
2294 fn editor_insert_link_repoints_an_autolink() {
2295 for format in [Format::Markdown, Format::Djot] {
2300 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
2301 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
2302 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
2303
2304 let nodes = ed.nodes().expect("nodes");
2306 let url = nodes
2307 .iter()
2308 .find(|n| n.kind == "url")
2309 .expect("still an autolink");
2310 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
2311 assert!(!nodes.iter().any(|n| n.kind == "link"));
2312 }
2313 }
2314
2315 #[test]
2316 fn editor_insert_link_escapes_the_destination() {
2317 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
2320 dj.insert_link(0, 1, "a)b").expect("link");
2321 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
2322
2323 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
2327 md.insert_link(0, 1, "a b").expect("link");
2328 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
2329
2330 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
2331 dj2.insert_link(0, 1, "a b").expect("link");
2332 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
2333 }
2334
2335 #[test]
2336 fn editor_insert_image_escapes_the_destination_per_format() {
2337 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
2340 md.insert_image(0, 1, "my cat.png").expect("image");
2341 assert_eq!(md.source_str().unwrap(), "\n");
2342
2343 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
2344 dj.insert_image(0, 1, "my cat.png").expect("image");
2345 assert_eq!(dj.source_str().unwrap(), "\n");
2346
2347 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
2349 paren.insert_image(0, 1, "a)b.png").expect("image");
2350 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
2351 }
2352
2353 #[test]
2354 fn editor_insert_image_keeps_an_empty_alt_empty() {
2355 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
2358 ed.insert_image(1, 1, "cat.png").expect("image");
2359 assert_eq!(ed.source_str().unwrap(), "ab\n");
2360 }
2361
2362 #[test]
2363 fn editor_insert_image_rejects_a_newline_destination() {
2364 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
2365 assert_eq!(ed.insert_image(0, 1, "a\nb.png"), Err(Error::InvalidArgument));
2366
2367 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2368 assert_eq!(xml.insert_image(3, 5, "x.png"), Err(Error::UnsupportedFormat));
2369 }
2370
2371 #[test]
2372 fn editor_insert_link_rejects_a_newline_destination() {
2373 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
2374 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
2375
2376 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2377 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
2378 }
2379
2380 #[test]
2381 fn editor_insert_literal_keeps_typed_specials_literal() {
2382 for format in [Format::Markdown, Format::Djot] {
2383 let mut ed = Editor::new_str("z\n", format).expect("editor");
2384 ed.insert_literal(0, "*hi*").expect("literal");
2386
2387 let nodes = ed.nodes().expect("nodes");
2389 assert!(!nodes.iter().any(|n| n.kind == "emph" || n.kind == "strong"));
2390 let text: String = nodes
2391 .iter()
2392 .filter(|n| n.kind == "str")
2393 .filter_map(|n| n.text.clone())
2394 .collect();
2395 assert_eq!(text, "*hi*z");
2396 }
2397 }
2398
2399 #[test]
2400 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
2401 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
2403 ed.insert_literal(1, "# ").expect("literal");
2404 assert_eq!(ed.source_str().unwrap(), "a# z\n");
2405
2406 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
2408 ed2.insert_literal(0, "# ").expect("literal");
2409 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
2410 assert!(!ed2.nodes().expect("nodes").iter().any(|n| n.kind == "heading"));
2411 }
2412
2413 #[test]
2414 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
2415 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
2416 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
2417
2418 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2419 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
2420 }
2421
2422 #[test]
2423 fn editor_insert_line_break_splices_in_cell_br() {
2424 let mut ed =
2425 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
2426 ed.insert_line_break(3).expect("line break");
2428 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
2429 let nodes = ed.nodes().expect("nodes");
2431 assert!(nodes.iter().any(|n| n.kind == "hard_break"));
2432 assert!(!nodes.iter().any(|n| n.kind == "raw_inline"));
2433 }
2434
2435 #[test]
2436 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
2437 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
2439 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
2440
2441 let mut dj =
2443 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
2444 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
2445
2446 let mut ed =
2448 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
2449 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
2450 }
2451
2452 #[test]
2453 fn editor_undo_redo_round_trip() {
2454 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2455 ed.edit_range(5, 5, "!").expect("edit");
2456 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2457
2458 let change = ed.undo().expect("undo ok").expect("something to undo");
2459 assert_eq!(ed.source_str().unwrap(), "hello\n");
2460 assert_eq!(change.new.end, 5);
2461 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
2462
2463 ed.redo().expect("redo ok").expect("something to redo");
2464 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2465 }
2466
2467 #[test]
2468 fn editor_coalesce_folds_a_run() {
2469 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2470 ed.edit_range(0, 0, "a").expect("edit");
2471 ed.edit_range(1, 1, "b").expect("edit");
2472 ed.coalesce_last_undo().expect("coalesce");
2473 assert_eq!(ed.source_str().unwrap(), "ab\n");
2474 ed.undo().expect("undo ok").expect("something to undo");
2476 assert_eq!(ed.source_str().unwrap(), "\n");
2477 assert!(ed.undo().expect("undo ok").is_none());
2478 }
2479
2480 #[test]
2481 fn editor_revision_bumps_per_successful_mutation() {
2482 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
2483 assert_eq!(ed.revision(), 0);
2484 ed.edit_range(1, 1, "y").expect("edit");
2485 assert_eq!(ed.revision(), 1);
2486
2487 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
2489 assert_eq!(xml.revision(), 0);
2490 assert!(xml.replace_content("0", "<b>").is_err());
2491 assert_eq!(xml.revision(), 0);
2492
2493 ed.undo().expect("undo ok").expect("something to undo");
2495 assert_eq!(ed.revision(), 2);
2496 ed.redo().expect("redo ok").expect("something to redo");
2497 assert_eq!(ed.revision(), 3);
2498 }
2499
2500 #[test]
2501 fn editor_dirty_range_tracks_and_clears() {
2502 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
2503 assert_eq!(ed.dirty_range(), None);
2505
2506 ed.edit_range(2, 2, "XY").expect("edit");
2508 assert_eq!(ed.dirty_range(), Some(2..4));
2509
2510 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
2514 assert!(d.start <= 2 && d.end >= 10, "range {d:?} must cover both edits");
2515
2516 let rev = ed.revision();
2518 ed.clear_dirty();
2519 assert_eq!(ed.dirty_range(), None);
2520 assert_eq!(ed.revision(), rev);
2521
2522 ed.undo().expect("undo ok").expect("something to undo");
2524 assert!(ed.dirty_range().is_some());
2525 }
2526
2527 #[test]
2528 fn editor_caret_blob_follows_undo_and_redo() {
2529 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2530 assert!(ed.caret_blob().unwrap().is_empty());
2531
2532 ed.set_caret_blob(b"before").expect("set caret");
2534 ed.edit_range(5, 5, "!").expect("edit");
2535 assert!(ed.caret_blob().unwrap().is_empty());
2537 ed.set_caret_blob(b"after").expect("set caret");
2538
2539 ed.undo().expect("undo ok").expect("something to undo");
2541 assert_eq!(ed.source_str().unwrap(), "hello\n");
2542 assert_eq!(ed.caret_blob().unwrap(), b"before");
2543
2544 ed.redo().expect("redo ok").expect("something to redo");
2546 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2547 assert_eq!(ed.caret_blob().unwrap(), b"after");
2548 }
2549
2550 #[test]
2551 fn editor_coalesced_run_keeps_the_pre_run_caret() {
2552 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2553 ed.set_caret_blob(b"c0").expect("set caret");
2554 ed.edit_range(0, 0, "a").expect("edit");
2555 ed.set_caret_blob(b"c1").expect("set caret");
2556 ed.edit_range(1, 1, "b").expect("edit");
2557 ed.coalesce_last_undo().expect("coalesce");
2558 ed.set_caret_blob(b"c2").expect("set caret");
2559
2560 ed.undo().expect("undo ok").expect("something to undo");
2562 assert_eq!(ed.source_str().unwrap(), "\n");
2563 assert_eq!(ed.caret_blob().unwrap(), b"c0");
2564 }
2565
2566 #[test]
2567 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
2568 let mut ed =
2569 Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
2570 ed.renumber_ordered_lists(0).expect("renumber ok");
2571 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
2572 }
2573
2574 #[test]
2575 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
2576 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
2577 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
2578 }
2579
2580 #[test]
2581 fn editor_table_insert_row_and_set_alignment() {
2582 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
2583 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
2584 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
2586 ed.source_str().unwrap(),
2587 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
2588 );
2589 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
2591 }
2592
2593 #[test]
2594 fn editor_table_edit_off_a_table_is_not_found() {
2595 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
2596 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
2597 }
2598
2599 #[test]
2600 fn editor_set_block_converts_setext_heading() {
2601 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
2603 ed.set_block(0, BlockKind::Heading(1)).expect("setext to atx");
2604 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
2605 }
2606
2607 #[test]
2608 fn editor_unwrap_and_smart_delete() {
2609 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
2610 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
2612
2613 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
2614 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
2616 }
2617
2618 #[test]
2619 fn editor_directives_require_the_extension_flag() {
2620 let src = ":::vis{.public}\nhi\n:::\n";
2621 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
2624 assert_eq!(plain.query("directive").expect("query").len(), 0);
2625 let mut ext = Editor::new_ext(
2627 src.as_bytes(),
2628 Format::Markdown,
2629 MarkdownExtensions { directives: true, ..Default::default() },
2630 )
2631 .expect("editor");
2632 assert_eq!(ext.query("directive").expect("query").len(), 1);
2633 }
2634
2635 #[test]
2636 fn document_html_elements_make_embedded_img_queryable() {
2637 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
2638 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
2640 assert_eq!(plain.query("image").expect("query").len(), 0);
2641 let mut ext = Document::parse_str_with(
2643 src,
2644 Format::Markdown,
2645 MarkdownExtensions { html_elements: true, ..Default::default() },
2646 )
2647 .expect("parse");
2648 let images = ext.query("image").expect("query");
2649 assert_eq!(images.len(), 1);
2650 assert_eq!(images[0].kind, "image");
2651 }
2652
2653 #[test]
2654 fn editor_filter_public_audience_view() {
2655 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
2656 let mut ed = Editor::new_ext(
2657 src.as_bytes(),
2658 Format::Markdown,
2659 MarkdownExtensions { directives: true, ..Default::default() },
2660 )
2661 .expect("editor");
2662 ed.filter(
2664 "directive[name=vis]",
2665 Some("directive[class~=public]"),
2666 true,
2667 )
2668 .expect("filter");
2669 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
2670 }
2671
2672 #[test]
2673 fn editor_filter_rejects_a_malformed_selector() {
2674 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2675 assert_eq!(ed.filter("list >", None, false), Err(Error::InvalidArgument));
2676 }
2677
2678 #[test]
2679 fn builder_builds_and_renders_a_document() {
2680 let mut b = Builder::new().expect("builder");
2681
2682 let title = b.add_text(TextKind::Str, "Title").unwrap();
2684 let heading = b.add_heading(1).unwrap();
2685 b.set_children(heading, &[title]).unwrap();
2686
2687 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
2688 let world = b.add_text(TextKind::Str, "world").unwrap();
2689 let emph = b.add(VoidKind::Emph).unwrap();
2690 b.set_children(emph, &[world]).unwrap();
2691 let para = b.add(VoidKind::Para).unwrap();
2692 b.set_children(para, &[hello, emph]).unwrap();
2693
2694 let doc = b.add(VoidKind::Doc).unwrap();
2695 b.set_children(doc, &[heading, para]).unwrap();
2696
2697 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
2698 assert!(html.contains("<h1>Title</h1>"), "{html}");
2699 assert!(html.contains("<em>world</em>"), "{html}");
2700
2701 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
2702 assert!(md.contains("# Title"), "{md}");
2703 assert!(md.contains("*world*"), "{md}");
2704
2705 let matches = b.query(doc, "heading").unwrap();
2706 assert_eq!(matches.len(), 1);
2707 assert_eq!(matches[0].kind, "heading");
2708
2709 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
2710 assert!(json.contains("\"kind\": \"doc\""), "{json}");
2711 }
2712
2713 #[test]
2714 fn builder_element_with_attributes() {
2715 let mut b = Builder::new().expect("builder");
2716 let inner = b.add_text(TextKind::Str, "hi").unwrap();
2717 let el = b.add_element("section").unwrap();
2718 b.set_children(el, &[inner]).unwrap();
2719 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)]).unwrap();
2720
2721 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
2722 assert!(html.contains("<section"), "{html}");
2723 assert!(html.contains("class=\"note\""), "{html}");
2724 assert!(html.contains("hidden"), "{html}");
2725 }
2726
2727 #[test]
2728 fn builder_lists_round_trip_to_markdown() {
2729 let mut b = Builder::new().expect("builder");
2730
2731 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
2733 let one_para = b.add(VoidKind::Para).unwrap();
2734 b.set_children(one_para, &[one_txt]).unwrap();
2735 let one = b.add(VoidKind::ListItem).unwrap();
2736 b.set_children(one, &[one_para]).unwrap();
2737
2738 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
2739 let two_para = b.add(VoidKind::Para).unwrap();
2740 b.set_children(two_para, &[two_txt]).unwrap();
2741 let two = b.add(VoidKind::ListItem).unwrap();
2742 b.set_children(two, &[two_para]).unwrap();
2743
2744 let list = b
2745 .add_ordered_list(OrderedNumbering::Decimal, OrderedDelim::Period, true, Some(1))
2746 .unwrap();
2747 b.set_children(list, &[one, two]).unwrap();
2748 let doc = b.add(VoidKind::Doc).unwrap();
2749 b.set_children(doc, &[list]).unwrap();
2750
2751 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
2752 assert!(md.contains("1. one"), "{md}");
2753 assert!(md.contains("2. two"), "{md}");
2754 }
2755
2756 #[test]
2757 fn builder_rejects_invalid_kind_and_id() {
2758 let b = Builder::new().expect("builder");
2759 let mut id = 0u32;
2763 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
2764 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2765
2766 let mut ptr = std::ptr::null();
2768 let mut len = 0usize;
2769 let status = unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
2770 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2771 }
2772}