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>,
113 pub attrs: Vec<(String, Option<String>)>,
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub enum InlineKind {
125 Strong,
126 Emph,
127 Verbatim,
128 Mark,
129 Superscript,
130 Subscript,
131 Insert,
132 Delete,
133}
134
135impl InlineKind {
136 fn to_c(self) -> c_int {
137 match self {
138 InlineKind::Strong => 0,
139 InlineKind::Emph => 1,
140 InlineKind::Verbatim => 2,
141 InlineKind::Mark => 3,
142 InlineKind::Superscript => 4,
143 InlineKind::Subscript => 5,
144 InlineKind::Insert => 6,
145 InlineKind::Delete => 7,
146 }
147 }
148}
149
150#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152pub enum BlockKind {
153 Paragraph,
154 Heading(u32),
156}
157
158impl BlockKind {
159 fn to_c(self) -> (c_int, u32) {
161 match self {
162 BlockKind::Paragraph => (0, 0),
163 BlockKind::Heading(level) => (1, level),
164 }
165 }
166}
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub enum BlockContainerKind {
175 BlockQuote,
176 BulletList,
177 OrderedList,
178}
179
180impl BlockContainerKind {
181 fn to_c(self) -> c_int {
182 match self {
183 BlockContainerKind::BlockQuote => 0,
184 BlockContainerKind::BulletList => 1,
185 BlockContainerKind::OrderedList => 2,
186 }
187 }
188}
189
190#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub struct Version {
192 pub major: u8,
193 pub minor: u8,
194 pub patch: u8,
195}
196
197pub fn version() -> Version {
198 let packed = unsafe { ffi::twig_version() };
199 Version {
200 major: (packed >> 16) as u8,
201 minor: (packed >> 8) as u8,
202 patch: packed as u8,
203 }
204}
205
206pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
212
213pub fn abi_version() -> u32 {
219 unsafe { ffi::twig_abi_version() }
220}
221
222pub fn version_string() -> &'static str {
223 let ptr = unsafe { ffi::twig_version_string() };
224 unsafe { std::ffi::CStr::from_ptr(ptr) }
225 .to_str()
226 .unwrap_or("")
227}
228
229#[derive(Debug)]
230pub struct Document {
231 raw: NonNull<ffi::TwigDocument>,
232}
233
234impl Document {
235 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
236 Self::parse_with(input, format, MarkdownExtensions::default())
237 }
238
239 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
240 Self::parse(input.as_bytes(), format)
241 }
242
243 pub fn parse_with(
249 input: &[u8],
250 format: Format,
251 extensions: MarkdownExtensions,
252 ) -> Result<Self, Error> {
253 let mut raw = std::ptr::null_mut();
254 let ffi_format: ffi::TwigFormat = format.into();
255 let status = unsafe {
256 ffi::twig_parse_ext(
257 input.as_ptr(),
258 input.len(),
259 ffi_format as i32,
260 extensions.to_flags(),
261 &mut raw,
262 )
263 };
264 Error::from_status(status)?;
265 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
266 Ok(Self { raw })
267 }
268
269 pub fn parse_str_with(
271 input: &str,
272 format: Format,
273 extensions: MarkdownExtensions,
274 ) -> Result<Self, Error> {
275 Self::parse_with(input.as_bytes(), format, extensions)
276 }
277
278 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
281 let raw = self.raw.as_ptr();
282 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
283 }
284
285 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
291 let raw = self.raw.as_ptr();
292 let ffi_format: ffi::TwigFormat = format.into();
293 collect_bytes(|ptr, len| unsafe {
294 ffi::twig_document_serialize(raw, ffi_format as i32, ptr, len)
295 })
296 }
297
298 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
301 let raw = self.raw.as_ptr();
302 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
303 }
304
305 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
314 let raw = self.raw.as_ptr();
315 collect_matches(|ptr, len| unsafe {
316 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
317 })
318 }
319}
320
321impl Drop for Document {
322 fn drop(&mut self) {
323 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
324 }
325}
326
327#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
333pub struct MarkdownExtensions {
334 pub directives: bool,
336 pub math: bool,
338 pub html_elements: bool,
343}
344
345impl MarkdownExtensions {
346 fn to_flags(self) -> u32 {
347 let mut flags = 0;
348 if self.directives {
349 flags |= ffi::TWIG_MD_DIRECTIVES;
350 }
351 if self.math {
352 flags |= ffi::TWIG_MD_MATH;
353 }
354 if self.html_elements {
355 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
356 }
357 flags
358 }
359}
360
361#[derive(Debug)]
367pub struct Editor {
368 raw: NonNull<ffi::TwigEditor>,
369}
370
371impl Editor {
372 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
375 let mut raw = std::ptr::null_mut();
376 let ffi_format: ffi::TwigFormat = format.into();
377 let status =
378 unsafe { ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
379 Error::from_status(status)?;
380 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
381 Ok(Self { raw })
382 }
383
384 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
385 Self::new(input.as_bytes(), format)
386 }
387
388 pub fn new_ext(input: &[u8], format: Format, extensions: MarkdownExtensions) -> Result<Self, Error> {
393 let mut raw = std::ptr::null_mut();
394 let ffi_format: ffi::TwigFormat = format.into();
395 let status = unsafe {
396 ffi::twig_editor_create_ext(
397 input.as_ptr(),
398 input.len(),
399 ffi_format as i32,
400 extensions.to_flags(),
401 &mut raw,
402 )
403 };
404 Error::from_status(status)?;
405 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
406 Ok(Self { raw })
407 }
408
409 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
411 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
412 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
413 })
414 }
415
416 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
419 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
420 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
421 })
422 }
423
424 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
426 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
427 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
428 })
429 }
430
431 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
433 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
434 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
435 })
436 }
437
438 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
441 let status = unsafe {
442 ffi::twig_editor_insert_child(
443 self.raw.as_ptr(),
444 locator.as_ptr(),
445 locator.len(),
446 index,
447 text.as_ptr(),
448 text.len(),
449 )
450 };
451 Error::from_status(status)
452 }
453
454 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
457 let status = unsafe {
458 ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len())
459 };
460 Error::from_status(status)
461 }
462
463 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
466 let status = unsafe {
467 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
468 };
469 Error::from_status(status)
470 }
471
472 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
476 let status = unsafe {
477 ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len())
478 };
479 Error::from_status(status)
480 }
481
482 pub fn filter(&mut self, drop: &str, keep: Option<&str>, unwrap_kept: bool) -> Result<(), Error> {
487 let (keep_ptr, keep_len) = match keep {
488 Some(k) => (k.as_ptr(), k.len()),
489 None => (std::ptr::null(), 0),
490 };
491 let status = unsafe {
492 ffi::twig_editor_filter(
493 self.raw.as_ptr(),
494 drop.as_ptr(),
495 drop.len(),
496 keep_ptr,
497 keep_len,
498 unwrap_kept as i32,
499 )
500 };
501 Error::from_status(status)
502 }
503
504 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
506 let raw = self.raw.as_ptr();
507 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
508 }
509
510 pub fn source_str(&mut self) -> Result<String, Error> {
512 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
513 }
514
515 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
518 let raw = self.raw.as_ptr();
519 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
520 }
521
522 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
525 let raw = self.raw.as_ptr();
526 collect_matches(|ptr, len| unsafe {
527 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
528 })
529 }
530
531 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
541 let mut change = ffi::TwigChange {
542 old_span: ffi::TwigSpan { start: 0, end: 0 },
543 new_span: ffi::TwigSpan { start: 0, end: 0 },
544 };
545 let status = unsafe {
546 ffi::twig_editor_edit_range(
547 self.raw.as_ptr(),
548 start,
549 end,
550 text.as_ptr(),
551 text.len(),
552 &mut change,
553 )
554 };
555 Error::from_status(status)?;
556 Ok(Change::from_ffi(change))
557 }
558
559 pub fn last_change(&mut self) -> Option<Change> {
565 let mut change = ffi::TwigChange {
566 old_span: ffi::TwigSpan { start: 0, end: 0 },
567 new_span: ffi::TwigSpan { start: 0, end: 0 },
568 };
569 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
570 match status.0 {
571 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
572 _ => None,
573 }
574 }
575
576 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
581 let mut change = ffi::TwigChange {
582 old_span: ffi::TwigSpan { start: 0, end: 0 },
583 new_span: ffi::TwigSpan { start: 0, end: 0 },
584 };
585 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
586 if status.0 == ffi::TwigStatus::NOT_FOUND {
587 return Ok(None);
588 }
589 Error::from_status(status)?;
590 Ok(Some(Change::from_ffi(change)))
591 }
592
593 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
597 let mut change = ffi::TwigChange {
598 old_span: ffi::TwigSpan { start: 0, end: 0 },
599 new_span: ffi::TwigSpan { start: 0, end: 0 },
600 };
601 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
602 if status.0 == ffi::TwigStatus::NOT_FOUND {
603 return Ok(None);
604 }
605 Error::from_status(status)?;
606 Ok(Some(Change::from_ffi(change)))
607 }
608
609 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
614 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
615 Error::from_status(status)
616 }
617
618 pub fn revision(&mut self) -> u64 {
624 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
625 }
626
627 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
648 let mut span = ffi::TwigSpan { start: 0, end: 0 };
649 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
650 match status.0 {
651 ffi::TwigStatus::OK => Some(span.start..span.end),
652 _ => None,
653 }
654 }
655
656 pub fn clear_dirty(&mut self) {
661 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
662 }
663
664 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
672 let status =
673 unsafe { ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len()) };
674 Error::from_status(status)
675 }
676
677 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
682 let raw = self.raw.as_ptr();
683 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
684 }
685
686 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
691 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
692 let mut len = 0usize;
693 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
694 Error::from_status(status)?;
695 if len == 0 {
696 return Ok(Vec::new());
697 }
698 if ptr.is_null() {
699 return Err(Error::Internal);
700 }
701 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
702 raw.iter().map(flat_node_from_ffi).collect()
703 }
704
705 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
712 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
713 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
714 let mut len = 0usize;
715 let status =
716 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
717 Error::from_status(status)?;
718 if len == 0 || ptr.is_null() {
719 return Ok(Vec::new());
720 }
721 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
722 raw.iter().map(query_match_from_ffi).collect()
723 }
724
725 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
733 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
734 let mut len = 0usize;
735 let status =
736 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
737 Error::from_status(status)?;
738 if len == 0 || ptr.is_null() {
739 return Ok(Vec::new());
740 }
741 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
742 raw.iter().map(flat_node_from_ffi).collect()
743 }
744
745 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
750 let mut m = ffi::TwigQueryMatch {
751 node_id: 0,
752 span: ffi::TwigSpan { start: 0, end: 0 },
753 content_span: ffi::TwigSpan { start: 0, end: 0 },
754 has_content_span: 0,
755 kind: std::ptr::null(),
756 };
757 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
758 match status.0 {
759 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
760 ffi::TwigStatus::NOT_FOUND => Ok(None),
761 _ => Err(Error::from_status(status).unwrap_err()),
762 }
763 }
764
765 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
769 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
770 let mut len = 0usize;
771 let status =
772 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
773 match status.0 {
774 ffi::TwigStatus::OK => {}
775 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
776 _ => return Err(Error::from_status(status).unwrap_err()),
777 }
778 if len == 0 || ptr.is_null() {
779 return Ok(Vec::new());
780 }
781 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
782 raw.iter().map(query_match_from_ffi).collect()
783 }
784
785 pub fn wrap_range(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
793 self.change_op(|ed, out| unsafe {
794 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
795 })
796 }
797
798 pub fn toggle_inline(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
803 self.change_op(|ed, out| unsafe {
804 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
805 })
806 }
807
808 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
815 let (block_kind, level) = kind.to_c();
816 self.change_op(|ed, out| unsafe {
817 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
818 })
819 }
820
821 pub fn toggle_block_container(
844 &mut self,
845 start: usize,
846 end: usize,
847 kind: BlockContainerKind,
848 ) -> Result<Change, Error> {
849 self.change_op(|ed, out| unsafe {
850 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
851 })
852 }
853
854 pub fn insert_link(
899 &mut self,
900 start: usize,
901 end: usize,
902 destination: &str,
903 ) -> Result<Change, Error> {
904 self.change_op(|ed, out| unsafe {
905 ffi::twig_editor_insert_link(
906 ed,
907 start,
908 end,
909 destination.as_ptr(),
910 destination.len(),
911 out,
912 )
913 })
914 }
915
916 fn change_op(
919 &mut self,
920 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
921 ) -> Result<Change, Error> {
922 let mut change = ffi::TwigChange {
923 old_span: ffi::TwigSpan { start: 0, end: 0 },
924 new_span: ffi::TwigSpan { start: 0, end: 0 },
925 };
926 let status = op(self.raw.as_ptr(), &mut change);
927 Error::from_status(status)?;
928 Ok(Change::from_ffi(change))
929 }
930
931 fn apply(
933 &mut self,
934 locator: &str,
935 text: &str,
936 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
937 ) -> Result<(), Error> {
938 let status = op(
939 self.raw.as_ptr(),
940 locator.as_ptr(),
941 locator.len(),
942 text.as_ptr(),
943 text.len(),
944 );
945 Error::from_status(status)
946 }
947}
948
949impl Drop for Editor {
950 fn drop(&mut self) {
951 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
952 }
953}
954
955fn collect_bytes(
960 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
961) -> Result<Vec<u8>, Error> {
962 let mut ptr = std::ptr::null();
963 let mut len = 0usize;
964 let status = call(&mut ptr, &mut len);
965 Error::from_status(status)?;
966 if len == 0 {
967 return Ok(Vec::new());
968 }
969 if ptr.is_null() {
970 return Err(Error::Internal);
971 }
972 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
973 Ok(bytes.to_vec())
974}
975
976fn collect_matches(
979 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
980) -> Result<Vec<QueryMatch>, Error> {
981 let mut ptr = std::ptr::null();
982 let mut len = 0usize;
983 let status = call(&mut ptr, &mut len);
984 Error::from_status(status)?;
985 if len == 0 {
986 return Ok(Vec::new());
987 }
988 if ptr.is_null() {
989 return Err(Error::Internal);
990 }
991 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
992 matches.iter().map(query_match_from_ffi).collect()
993}
994
995fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
998 Ok(QueryMatch {
999 node_id: m.node_id,
1000 span: m.span.start..m.span.end,
1001 content_span: if m.has_content_span != 0 {
1002 Some(m.content_span.start..m.content_span.end)
1003 } else {
1004 None
1005 },
1006 kind: borrowed_cstr(m.kind)?,
1007 })
1008}
1009
1010fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
1012 let node_id = |v: u32| if v == ffi::TWIG_NO_NODE { None } else { Some(NodeId(v)) };
1013 Ok(FlatNode {
1014 id: NodeId(n.id),
1015 parent: node_id(n.parent),
1016 first_child: node_id(n.first_child),
1017 next_sibling: node_id(n.next_sibling),
1018 span: n.span.start..n.span.end,
1019 content_span: if n.has_content_span != 0 {
1020 Some(n.content_span.start..n.content_span.end)
1021 } else {
1022 None
1023 },
1024 level: if n.level != 0 { Some(n.level) } else { None },
1025 kind: borrowed_cstr(n.kind)?,
1026 text: borrowed_bytes(n.text_ptr, n.text_len),
1027 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
1028 head: match n.head {
1029 ffi::TWIG_HEAD_NONE => None,
1030 v => Some(v != 0),
1031 },
1032 alignment: Alignment::from_c(n.alignment),
1033 name: borrowed_bytes(n.name_ptr, n.name_len),
1034 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
1035 })
1036}
1037
1038fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
1042 if ptr.is_null() || len == 0 {
1043 return Vec::new();
1044 }
1045 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
1046 kvs.iter()
1047 .map(|kv| {
1048 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
1049 (key, borrowed_bytes(kv.value, kv.value_len))
1050 })
1051 .collect()
1052}
1053
1054fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
1056 if ptr.is_null() {
1057 return Err(Error::Internal);
1058 }
1059 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
1060 .to_str()
1061 .map_err(|_| Error::Internal)?
1062 .to_owned())
1063}
1064
1065fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
1069 if ptr.is_null() {
1070 return None;
1071 }
1072 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1073 Some(String::from_utf8_lossy(bytes).into_owned())
1074}
1075
1076#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1080pub struct NodeId(pub u32);
1081
1082#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1085pub enum VoidKind {
1086 Doc,
1087 Para,
1088 ThematicBreak,
1089 Section,
1090 Div,
1091 BlockQuote,
1092 DefinitionList,
1093 Table,
1094 ListItem,
1095 DefinitionListItem,
1096 Term,
1097 Definition,
1098 Caption,
1099 SoftBreak,
1100 HardBreak,
1101 NonBreakingSpace,
1102 Emph,
1103 Strong,
1104 Span,
1105 Mark,
1106 Superscript,
1107 Subscript,
1108 Insert,
1109 Delete,
1110 DoubleQuoted,
1111 SingleQuoted,
1112}
1113
1114impl VoidKind {
1115 fn to_c(self) -> c_int {
1116 match self {
1118 VoidKind::Doc => 0,
1119 VoidKind::Para => 1,
1120 VoidKind::ThematicBreak => 3,
1121 VoidKind::Section => 4,
1122 VoidKind::Div => 5,
1123 VoidKind::BlockQuote => 9,
1124 VoidKind::DefinitionList => 13,
1125 VoidKind::Table => 14,
1126 VoidKind::ListItem => 15,
1127 VoidKind::DefinitionListItem => 17,
1128 VoidKind::Term => 18,
1129 VoidKind::Definition => 19,
1130 VoidKind::Caption => 22,
1131 VoidKind::SoftBreak => 26,
1132 VoidKind::HardBreak => 27,
1133 VoidKind::NonBreakingSpace => 28,
1134 VoidKind::Emph => 38,
1135 VoidKind::Strong => 39,
1136 VoidKind::Span => 42,
1137 VoidKind::Mark => 43,
1138 VoidKind::Superscript => 44,
1139 VoidKind::Subscript => 45,
1140 VoidKind::Insert => 46,
1141 VoidKind::Delete => 47,
1142 VoidKind::DoubleQuoted => 48,
1143 VoidKind::SingleQuoted => 49,
1144 }
1145 }
1146}
1147
1148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1150pub enum TextKind {
1151 Str,
1152 Symb,
1153 Verbatim,
1154 InlineMath,
1155 DisplayMath,
1156 Url,
1157 Email,
1158 FootnoteReference,
1159 Comment,
1160 Doctype,
1161 Cdata,
1162}
1163
1164impl TextKind {
1165 fn to_c(self) -> c_int {
1166 match self {
1167 TextKind::Str => 25,
1168 TextKind::Symb => 29,
1169 TextKind::Verbatim => 30,
1170 TextKind::InlineMath => 32,
1171 TextKind::DisplayMath => 33,
1172 TextKind::Url => 34,
1173 TextKind::Email => 35,
1174 TextKind::FootnoteReference => 36,
1175 TextKind::Comment => 52,
1176 TextKind::Doctype => 53,
1177 TextKind::Cdata => 55,
1178 }
1179 }
1180}
1181
1182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1184pub enum BulletStyle {
1185 Dash,
1186 Plus,
1187 Star,
1188}
1189
1190impl BulletStyle {
1191 fn to_c(self) -> c_int {
1192 match self {
1193 BulletStyle::Dash => 0,
1194 BulletStyle::Plus => 1,
1195 BulletStyle::Star => 2,
1196 }
1197 }
1198}
1199
1200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1202pub enum OrderedNumbering {
1203 Decimal,
1204 LowerAlpha,
1205 UpperAlpha,
1206 LowerRoman,
1207 UpperRoman,
1208}
1209
1210impl OrderedNumbering {
1211 fn to_c(self) -> c_int {
1212 match self {
1213 OrderedNumbering::Decimal => 0,
1214 OrderedNumbering::LowerAlpha => 1,
1215 OrderedNumbering::UpperAlpha => 2,
1216 OrderedNumbering::LowerRoman => 3,
1217 OrderedNumbering::UpperRoman => 4,
1218 }
1219 }
1220}
1221
1222#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1224pub enum OrderedDelim {
1225 Period,
1226 ParenAfter,
1227 ParenBoth,
1228}
1229
1230impl OrderedDelim {
1231 fn to_c(self) -> c_int {
1232 match self {
1233 OrderedDelim::Period => 0,
1234 OrderedDelim::ParenAfter => 1,
1235 OrderedDelim::ParenBoth => 2,
1236 }
1237 }
1238}
1239
1240#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1243pub enum Alignment {
1244 Default,
1245 Left,
1246 Right,
1247 Center,
1248}
1249
1250impl Alignment {
1251 fn to_c(self) -> c_int {
1252 match self {
1253 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
1254 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
1255 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
1256 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
1257 }
1258 }
1259
1260 fn from_c(v: c_int) -> Option<Self> {
1263 match v {
1264 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
1265 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
1266 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
1267 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
1268 _ => None,
1269 }
1270 }
1271}
1272
1273#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1275pub enum SmartPunctuation {
1276 LeftSingleQuote,
1277 RightSingleQuote,
1278 LeftDoubleQuote,
1279 RightDoubleQuote,
1280 Ellipses,
1281 EmDash,
1282 EnDash,
1283}
1284
1285impl SmartPunctuation {
1286 fn to_c(self) -> c_int {
1287 match self {
1288 SmartPunctuation::LeftSingleQuote => 0,
1289 SmartPunctuation::RightSingleQuote => 1,
1290 SmartPunctuation::LeftDoubleQuote => 2,
1291 SmartPunctuation::RightDoubleQuote => 3,
1292 SmartPunctuation::Ellipses => 4,
1293 SmartPunctuation::EmDash => 5,
1294 SmartPunctuation::EnDash => 6,
1295 }
1296 }
1297}
1298
1299#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1301pub enum DirectiveForm {
1302 Text,
1303 Leaf,
1304 Container,
1305}
1306
1307impl DirectiveForm {
1308 fn to_c(self) -> c_int {
1309 match self {
1310 DirectiveForm::Text => 0,
1311 DirectiveForm::Leaf => 1,
1312 DirectiveForm::Container => 2,
1313 }
1314 }
1315}
1316
1317fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
1321 match s {
1322 Some(x) => (x.as_ptr(), x.len(), 1),
1323 None => (std::ptr::null(), 0, 0),
1324 }
1325}
1326
1327#[derive(Debug)]
1334pub struct Builder {
1335 raw: NonNull<ffi::TwigBuilder>,
1336}
1337
1338impl Builder {
1339 pub fn new() -> Result<Self, Error> {
1341 let mut raw = std::ptr::null_mut();
1342 let status = unsafe { ffi::twig_builder_create(&mut raw) };
1343 Error::from_status(status)?;
1344 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1345 Ok(Self { raw })
1346 }
1347
1348 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
1351 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
1352 }
1353
1354 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
1356 self.emit(|b, out| unsafe { ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out) })
1357 }
1358
1359 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
1361 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
1362 }
1363
1364 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
1366 let (lp, ll, has) = opt_str(lang);
1367 self.emit(|b, out| unsafe { ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out) })
1368 }
1369
1370 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1372 self.emit(|b, out| unsafe {
1373 ffi::twig_builder_add_raw_block(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1374 })
1375 }
1376
1377 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
1379 self.emit(|b, out| unsafe {
1380 ffi::twig_builder_add_metadata(b, lang.as_ptr(), lang.len(), text.as_ptr(), text.len(), out)
1381 })
1382 }
1383
1384 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1386 self.emit(|b, out| unsafe {
1387 ffi::twig_builder_add_raw_inline(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1388 })
1389 }
1390
1391 pub fn add_smart_punctuation(&mut self, kind: SmartPunctuation, text: &str) -> Result<NodeId, Error> {
1393 self.emit(|b, out| unsafe {
1394 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
1395 })
1396 }
1397
1398 pub fn add_link(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1401 let (dp, dl, hd) = opt_str(destination);
1402 let (rp, rl, hr) = opt_str(reference);
1403 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
1404 }
1405
1406 pub fn add_image(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1408 let (dp, dl, hd) = opt_str(destination);
1409 let (rp, rl, hr) = opt_str(reference);
1410 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
1411 }
1412
1413 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
1415 self.emit(|b, out| unsafe { ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out) })
1416 }
1417
1418 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
1420 self.emit(|b, out| unsafe { ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out) })
1421 }
1422
1423 pub fn add_processing_instruction(&mut self, target: &str, data: &str) -> Result<NodeId, Error> {
1425 self.emit(|b, out| unsafe {
1426 ffi::twig_builder_add_processing_instruction(b, target.as_ptr(), target.len(), data.as_ptr(), data.len(), out)
1427 })
1428 }
1429
1430 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
1432 self.emit(|b, out| unsafe { ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out) })
1433 }
1434
1435 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
1437 self.emit(|b, out| unsafe {
1438 ffi::twig_builder_add_reference(b, label.as_ptr(), label.len(), destination.as_ptr(), destination.len(), out)
1439 })
1440 }
1441
1442 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
1444 self.emit(|b, out| unsafe { ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out) })
1445 }
1446
1447 pub fn add_ordered_list(
1449 &mut self,
1450 numbering: OrderedNumbering,
1451 delim: OrderedDelim,
1452 tight: bool,
1453 start: Option<u32>,
1454 ) -> Result<NodeId, Error> {
1455 let (start_val, has_start) = match start {
1456 Some(s) => (s, 1),
1457 None => (0, 0),
1458 };
1459 self.emit(|b, out| unsafe {
1460 ffi::twig_builder_add_ordered_list(b, numbering.to_c(), delim.to_c(), tight as c_int, start_val, has_start, out)
1461 })
1462 }
1463
1464 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
1466 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
1467 }
1468
1469 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
1471 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list_item(b, checked as c_int, out) })
1472 }
1473
1474 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
1476 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
1477 }
1478
1479 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
1481 self.emit(|b, out| unsafe { ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out) })
1482 }
1483
1484 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
1487 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
1488 let status = unsafe { ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len()) };
1489 Error::from_status(status)
1490 }
1491
1492 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
1496 let kvs: Vec<ffi::TwigKeyVal> = attrs
1497 .iter()
1498 .map(|(k, v)| ffi::TwigKeyVal {
1499 key: k.as_ptr(),
1500 key_len: k.len(),
1501 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
1502 value_len: v.map_or(0, |s| s.len()),
1503 })
1504 .collect();
1505 let status = unsafe { ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len()) };
1506 Error::from_status(status)
1507 }
1508
1509 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1512 let raw = self.raw.as_ptr();
1513 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
1514 }
1515
1516 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
1520 let raw = self.raw.as_ptr();
1521 let ffi_format: ffi::TwigFormat = format.into();
1522 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_serialize(raw, root.0, ffi_format as i32, ptr, len) })
1523 }
1524
1525 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1527 let raw = self.raw.as_ptr();
1528 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
1529 }
1530
1531 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1534 let raw = self.raw.as_ptr();
1535 collect_matches(|ptr, len| unsafe {
1536 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
1537 })
1538 }
1539
1540 fn emit(
1543 &mut self,
1544 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
1545 ) -> Result<NodeId, Error> {
1546 let mut id: u32 = 0;
1547 let status = call(self.raw.as_ptr(), &mut id);
1548 Error::from_status(status)?;
1549 Ok(NodeId(id))
1550 }
1551}
1552
1553impl Drop for Builder {
1554 fn drop(&mut self) {
1555 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
1556 }
1557}
1558
1559#[cfg(test)]
1560mod tests {
1561 use super::*;
1562
1563 #[test]
1564 fn abi_version_matches() {
1565 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
1569 }
1570
1571 #[test]
1572 fn parses_and_renders_markdown_html() {
1573 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1574 let html = doc.render_html().expect("render html");
1575 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
1576 }
1577
1578 #[test]
1579 fn parses_html_input() {
1580 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
1581 let html = doc.render_html().expect("render html");
1582 assert!(String::from_utf8_lossy(&html).contains("hi"));
1583 }
1584
1585 #[test]
1586 fn serialize_round_trips_and_cross_converts() {
1587 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1588
1589 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
1590 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
1591
1592 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
1594 }
1595
1596 #[test]
1597 fn serialize_markdown_to_djot() {
1598 let mut doc =
1599 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
1600 let djot = doc.serialize(Format::Djot).expect("serialize djot");
1601 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
1602 }
1603
1604 #[test]
1605 fn ast_json_dumps_the_tree() {
1606 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
1607 let json = doc.ast_json().expect("ast json");
1608 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1609 }
1610
1611 #[test]
1612 fn query_finds_nodes_by_selector() {
1613 let source = "# One\n\n## Two\n";
1614 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1615 let matches = doc.query("heading").expect("query");
1616
1617 assert_eq!(matches.len(), 2);
1618 for m in &matches {
1619 assert_eq!(m.kind, "heading");
1620 assert!(m.span.start < m.span.end);
1621 }
1622 }
1623
1624 #[test]
1625 fn query_recovers_code_spans() {
1626 let source = "prose `code` more prose\n";
1627 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1628 let matches = doc.query("verbatim").expect("query");
1629
1630 assert_eq!(matches.len(), 1);
1631 assert_eq!(&source[matches[0].span.clone()], "`code`");
1632 }
1633
1634 #[test]
1635 fn query_rejects_a_malformed_selector() {
1636 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
1637 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
1638 }
1639
1640 #[test]
1641 fn editor_edits_by_index_path() {
1642 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
1643 ed.replace_content("0.0", "bye").expect("replace_content");
1644 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
1645 }
1646
1647 #[test]
1648 fn flat_nodes_expose_element_name_and_attrs() {
1649 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
1653 let mut ed = Editor::new_ext(
1654 src.as_bytes(),
1655 Format::Markdown,
1656 MarkdownExtensions { html_elements: true, ..Default::default() },
1657 )
1658 .expect("editor");
1659 let nodes = ed.nodes().expect("nodes");
1660
1661 let source = nodes
1662 .iter()
1663 .find(|n| n.name.as_deref() == Some("source"))
1664 .expect("a <source> element node");
1665 assert_eq!(
1666 source.attrs,
1667 vec![
1668 ("media".to_string(), Some("(prefers-color-scheme: dark)".to_string())),
1669 ("srcset".to_string(), Some("d.svg".to_string())),
1670 ]
1671 );
1672
1673 let img = nodes.iter().find(|n| n.kind == "image").expect("an image node");
1676 assert!(img.name.is_none());
1677 assert_eq!(img.destination.as_deref(), Some("l.svg"));
1678
1679 let picture_kids_str = nodes.iter().find(|n| n.kind == "str");
1681 if let Some(s) = picture_kids_str {
1682 assert!(s.name.is_none() && s.attrs.is_empty());
1683 }
1684 }
1685
1686 #[test]
1687 fn editor_insert_child_and_delete() {
1688 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
1689 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1690 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
1691 ed.delete("0.1").expect("delete");
1692 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
1693 }
1694
1695 #[test]
1696 fn editor_edits_by_selector() {
1697 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1698 ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1699 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
1700 }
1701
1702 #[test]
1703 fn editor_locator_errors_are_distinct() {
1704 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
1705 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
1706 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
1707 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
1708 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
1710 }
1711
1712 #[test]
1713 fn editor_reparse_break_rolls_back() {
1714 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
1715 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
1716 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
1717 }
1718
1719 #[test]
1720 fn editor_leaf_content_is_not_editable() {
1721 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1722 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
1723 }
1724
1725 #[test]
1726 fn editor_query_reflects_current_tree() {
1727 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
1728 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1729 assert_eq!(ed.query("element").expect("query").len(), 3);
1731 let json = ed.ast_json().expect("ast_json");
1732 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1733 }
1734
1735 #[test]
1738 fn editor_edit_range_types_backspaces_and_reports_change() {
1739 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
1740
1741 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
1743 assert_eq!(ed.source_str().unwrap(), "aXb\n");
1744 assert_eq!(c.old, 1..1);
1745 assert_eq!(c.new, 1..2);
1746 assert_eq!(c.delta(), 1);
1747
1748 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
1750 assert_eq!(ed.source_str().unwrap(), "ab\n");
1751 assert_eq!(c2.old, 1..2);
1752 assert_eq!(c2.new, 1..1);
1753 assert_eq!(c2.delta(), -1);
1754 }
1755
1756 #[test]
1757 fn editor_edit_range_rejects_bad_ranges() {
1758 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1759 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"); }
1763
1764 #[test]
1765 fn editor_last_change_reports_locator_ops_too() {
1766 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1767 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1770 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
1771 let c = ed.last_change().expect("a change was recorded");
1772 assert_eq!(c.old, 7..13);
1774 assert_eq!(c.new, 7..17);
1775 }
1776
1777 #[test]
1778 fn editor_nodes_is_a_walkable_flat_tree() {
1779 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1780 let nodes = ed.nodes().expect("nodes");
1781 assert!(!nodes.is_empty());
1782
1783 for (i, n) in nodes.iter().enumerate() {
1785 assert_eq!(n.id, NodeId(i as u32));
1786 }
1787 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
1789 assert_eq!(roots.len(), 1);
1790 assert_eq!(roots[0].kind, "doc");
1791
1792 let heading = nodes.iter().find(|n| n.kind == "heading").expect("a heading");
1794 assert_eq!(heading.level, Some(1));
1795 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
1796
1797 assert_eq!(heading.head, None);
1799 assert_eq!(heading.alignment, None);
1800
1801 for n in nodes.iter().filter(|n| n.parent.is_some()) {
1804 let p = &nodes[n.parent.unwrap().0 as usize];
1805 let mut kid = p.first_child;
1806 let mut seen = false;
1807 while let Some(NodeId(k)) = kid {
1808 if k == n.id.0 {
1809 seen = true;
1810 break;
1811 }
1812 kid = nodes[k as usize].next_sibling;
1813 }
1814 assert!(seen, "node {:?} not found among its parent's children", n.id);
1815 }
1816 }
1817
1818 #[test]
1819 fn editor_child_spans_and_subtree_agree_with_nodes() {
1820 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
1821 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
1822 let all = ed.nodes().expect("nodes");
1823 let doc = all.iter().find(|n| n.kind == "doc").expect("doc");
1824
1825 let top = ed.child_spans(None).expect("child_spans");
1828 let mut want = Vec::new();
1829 let mut c = doc.first_child;
1830 while let Some(id) = c {
1831 want.push(id);
1832 c = all[id.0 as usize].next_sibling;
1833 }
1834 assert_eq!(top.len(), want.len(), "top-level count");
1835 for (m, id) in top.iter().zip(&want) {
1836 assert_eq!(m.node_id, id.0, "child id");
1837 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
1838 assert_eq!(m.span, all[id.0 as usize].span, "child span");
1839 }
1840 assert!(src[top[0].span.clone()].starts_with('#'), "first block is the heading");
1842
1843 let list = top.iter().find(|m| m.kind.ends_with("list")).expect("a list");
1845 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
1846 assert_eq!(items.len(), 2);
1847 assert!(items.iter().all(|m| m.kind == "list_item"), "items: {items:?}");
1848
1849 let para = top.iter().find(|m| m.kind == "para").expect("a para").node_id;
1851 let sub = ed.subtree(NodeId(para)).expect("subtree");
1852 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
1853 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
1854 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
1855 assert_eq!(sub[0].kind, "para");
1856 for (i, n) in sub.iter().enumerate() {
1857 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
1858 for link in [n.parent, n.first_child, n.next_sibling].into_iter().flatten() {
1859 assert!((link.0 as usize) < sub.len(), "link {link:?} escapes the subtree");
1860 }
1861 }
1862 assert!(
1863 src[sub[0].span.clone()].starts_with("Hello"),
1864 "absolute span: {:?}",
1865 &src[sub[0].span.clone()]
1866 );
1867
1868 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<String> {
1870 let mut out = Vec::new();
1871 let mut stack = vec![root];
1872 while let Some(id) = stack.pop() {
1873 let n = &all[id.0 as usize];
1874 out.push(n.kind.clone());
1875 let mut c = n.first_child;
1876 while let Some(cid) = c {
1877 stack.push(cid);
1878 c = all[cid.0 as usize].next_sibling;
1879 }
1880 }
1881 out
1882 }
1883 let mut want_kinds = arena_kinds(&all, NodeId(para));
1884 let mut got_kinds: Vec<String> = sub.iter().map(|n| n.kind.clone()).collect();
1885 want_kinds.sort();
1886 got_kinds.sort();
1887 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
1888
1889 assert!(matches!(ed.subtree(NodeId(9999)), Err(Error::InvalidArgument)));
1891 }
1892
1893 #[test]
1894 fn flat_nodes_carry_table_head_and_alignment() {
1895 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
1899 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
1900 let nodes = ed.nodes().expect("nodes");
1901
1902 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == "row").collect();
1903 assert_eq!(rows.len(), 2, "a header row and one body row");
1904 assert_eq!(rows[0].head, Some(true), "first row is the header");
1905 assert_eq!(rows[1].head, Some(false), "second row is a body row");
1906
1907 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == "cell").collect();
1908 assert_eq!(cells.len(), 4);
1909 assert_eq!(cells[0].alignment, Some(Alignment::Left));
1911 assert_eq!(cells[1].alignment, Some(Alignment::Right));
1912 assert_eq!(cells[2].alignment, Some(Alignment::Left));
1913 assert_eq!(cells[3].alignment, Some(Alignment::Right));
1914 assert_eq!(cells[0].head, Some(true));
1916 assert_eq!(cells[2].head, Some(false));
1917
1918 let mut plain = Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
1921 let pnodes = plain.nodes().expect("nodes");
1922 let pcell = pnodes.iter().find(|n| n.kind == "cell").expect("a cell");
1923 assert_eq!(pcell.alignment, Some(Alignment::Default));
1924 }
1925
1926 #[test]
1927 fn editor_node_at_and_ancestors_hit_test_offsets() {
1928 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1929
1930 let m = ed.node_at(2).expect("node_at").expect("a node covers offset 2");
1932 assert!(m.span.contains(&2));
1933
1934 let chain = ed.ancestors_at(2).expect("ancestors_at");
1936 assert!(!chain.is_empty());
1937 assert_eq!(chain[0].kind, "doc");
1938 assert_eq!(chain.last().unwrap().node_id, m.node_id);
1939
1940 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
1942 }
1943
1944 #[test]
1947 fn editor_wrap_and_toggle_inline_round_trip() {
1948 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
1949
1950 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
1952 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
1953 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
1954
1955 ed.toggle_inline(4, 8, InlineKind::Strong).expect("toggle off");
1957 assert_eq!(ed.source_str().unwrap(), "a word b\n");
1958
1959 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
1961 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
1962 }
1963
1964 #[test]
1965 fn editor_inline_kind_support_is_format_specific() {
1966 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
1968 assert_eq!(md.wrap_range(2, 6, InlineKind::Mark), Err(Error::UnsupportedFormat));
1969
1970 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
1972 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
1973 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
1974 }
1975
1976 #[test]
1977 fn editor_toggle_strips_verbatim_via_content_span() {
1978 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
1979 ed.toggle_inline(2, 8, InlineKind::Verbatim).expect("toggle code off");
1981 assert_eq!(ed.source_str().unwrap(), "a code b\n");
1982
1983 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
1986 ed2.toggle_inline(2, 7, InlineKind::Verbatim).expect("toggle multi off");
1987 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
1988 }
1989
1990 #[test]
1991 fn editor_set_block_switches_para_and_heading_levels() {
1992 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
1993
1994 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
1996 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
1997
1998 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
2000 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
2001
2002 ed.set_block(2, BlockKind::Paragraph).expect("to para");
2004 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
2005 }
2006
2007 #[test]
2008 fn editor_set_block_rejects_bad_level_and_format() {
2009 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2010 assert_eq!(md.set_block(0, BlockKind::Heading(9)), Err(Error::InvalidArgument));
2011
2012 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2013 assert_eq!(xml.set_block(1, BlockKind::Heading(1)), Err(Error::UnsupportedFormat));
2014 }
2015
2016 #[test]
2017 fn editor_toggle_block_container_round_trips() {
2018 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
2019
2020 let c = ed
2021 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
2022 .expect("quote on");
2023 assert_eq!(ed.source_str().unwrap(), "> a\n");
2024 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
2025
2026 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2027 .expect("quote off");
2028 assert_eq!(ed.source_str().unwrap(), "a\n");
2029 }
2030
2031 #[test]
2032 fn editor_toggle_block_container_nests_a_partial_selection() {
2033 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
2034
2035 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2038 .expect("nest");
2039 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
2040
2041 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
2043 .expect("peel");
2044 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
2045 }
2046
2047 #[test]
2048 fn editor_toggle_block_container_numbers_and_converts_lists() {
2049 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
2050
2051 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
2053 .expect("ordered on");
2054 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
2055
2056 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
2058 .expect("convert");
2059 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
2060 }
2061
2062 #[test]
2063 fn editor_toggle_block_container_rejects_unspellable_format() {
2064 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2065 assert_eq!(
2066 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
2067 Err(Error::UnsupportedFormat)
2068 );
2069 }
2070
2071 #[test]
2072 fn editor_insert_link_wraps_and_repoints() {
2073 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
2074
2075 ed.insert_link(2, 6, "http://x.dev").expect("link");
2076 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
2077
2078 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
2080 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
2081 }
2082
2083 #[test]
2084 fn editor_insert_link_repoints_an_autolink() {
2085 for format in [Format::Markdown, Format::Djot] {
2090 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
2091 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
2092 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
2093
2094 let nodes = ed.nodes().expect("nodes");
2096 let url = nodes
2097 .iter()
2098 .find(|n| n.kind == "url")
2099 .expect("still an autolink");
2100 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
2101 assert!(!nodes.iter().any(|n| n.kind == "link"));
2102 }
2103 }
2104
2105 #[test]
2106 fn editor_insert_link_escapes_the_destination() {
2107 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
2110 dj.insert_link(0, 1, "a)b").expect("link");
2111 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
2112
2113 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
2117 md.insert_link(0, 1, "a b").expect("link");
2118 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
2119
2120 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
2121 dj2.insert_link(0, 1, "a b").expect("link");
2122 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
2123 }
2124
2125 #[test]
2126 fn editor_insert_link_rejects_a_newline_destination() {
2127 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
2128 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
2129
2130 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2131 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
2132 }
2133
2134 #[test]
2135 fn editor_undo_redo_round_trip() {
2136 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2137 ed.edit_range(5, 5, "!").expect("edit");
2138 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2139
2140 let change = ed.undo().expect("undo ok").expect("something to undo");
2141 assert_eq!(ed.source_str().unwrap(), "hello\n");
2142 assert_eq!(change.new.end, 5);
2143 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
2144
2145 ed.redo().expect("redo ok").expect("something to redo");
2146 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2147 }
2148
2149 #[test]
2150 fn editor_coalesce_folds_a_run() {
2151 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2152 ed.edit_range(0, 0, "a").expect("edit");
2153 ed.edit_range(1, 1, "b").expect("edit");
2154 ed.coalesce_last_undo().expect("coalesce");
2155 assert_eq!(ed.source_str().unwrap(), "ab\n");
2156 ed.undo().expect("undo ok").expect("something to undo");
2158 assert_eq!(ed.source_str().unwrap(), "\n");
2159 assert!(ed.undo().expect("undo ok").is_none());
2160 }
2161
2162 #[test]
2163 fn editor_revision_bumps_per_successful_mutation() {
2164 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
2165 assert_eq!(ed.revision(), 0);
2166 ed.edit_range(1, 1, "y").expect("edit");
2167 assert_eq!(ed.revision(), 1);
2168
2169 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
2171 assert_eq!(xml.revision(), 0);
2172 assert!(xml.replace_content("0", "<b>").is_err());
2173 assert_eq!(xml.revision(), 0);
2174
2175 ed.undo().expect("undo ok").expect("something to undo");
2177 assert_eq!(ed.revision(), 2);
2178 ed.redo().expect("redo ok").expect("something to redo");
2179 assert_eq!(ed.revision(), 3);
2180 }
2181
2182 #[test]
2183 fn editor_dirty_range_tracks_and_clears() {
2184 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
2185 assert_eq!(ed.dirty_range(), None);
2187
2188 ed.edit_range(2, 2, "XY").expect("edit");
2190 assert_eq!(ed.dirty_range(), Some(2..4));
2191
2192 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
2196 assert!(d.start <= 2 && d.end >= 10, "range {d:?} must cover both edits");
2197
2198 let rev = ed.revision();
2200 ed.clear_dirty();
2201 assert_eq!(ed.dirty_range(), None);
2202 assert_eq!(ed.revision(), rev);
2203
2204 ed.undo().expect("undo ok").expect("something to undo");
2206 assert!(ed.dirty_range().is_some());
2207 }
2208
2209 #[test]
2210 fn editor_caret_blob_follows_undo_and_redo() {
2211 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2212 assert!(ed.caret_blob().unwrap().is_empty());
2213
2214 ed.set_caret_blob(b"before").expect("set caret");
2216 ed.edit_range(5, 5, "!").expect("edit");
2217 assert!(ed.caret_blob().unwrap().is_empty());
2219 ed.set_caret_blob(b"after").expect("set caret");
2220
2221 ed.undo().expect("undo ok").expect("something to undo");
2223 assert_eq!(ed.source_str().unwrap(), "hello\n");
2224 assert_eq!(ed.caret_blob().unwrap(), b"before");
2225
2226 ed.redo().expect("redo ok").expect("something to redo");
2228 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2229 assert_eq!(ed.caret_blob().unwrap(), b"after");
2230 }
2231
2232 #[test]
2233 fn editor_coalesced_run_keeps_the_pre_run_caret() {
2234 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2235 ed.set_caret_blob(b"c0").expect("set caret");
2236 ed.edit_range(0, 0, "a").expect("edit");
2237 ed.set_caret_blob(b"c1").expect("set caret");
2238 ed.edit_range(1, 1, "b").expect("edit");
2239 ed.coalesce_last_undo().expect("coalesce");
2240 ed.set_caret_blob(b"c2").expect("set caret");
2241
2242 ed.undo().expect("undo ok").expect("something to undo");
2244 assert_eq!(ed.source_str().unwrap(), "\n");
2245 assert_eq!(ed.caret_blob().unwrap(), b"c0");
2246 }
2247
2248 #[test]
2249 fn editor_set_block_converts_setext_heading() {
2250 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
2252 ed.set_block(0, BlockKind::Heading(1)).expect("setext to atx");
2253 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
2254 }
2255
2256 #[test]
2257 fn editor_unwrap_and_smart_delete() {
2258 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
2259 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
2261
2262 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
2263 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
2265 }
2266
2267 #[test]
2268 fn editor_directives_require_the_extension_flag() {
2269 let src = ":::vis{.public}\nhi\n:::\n";
2270 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
2273 assert_eq!(plain.query("directive").expect("query").len(), 0);
2274 let mut ext = Editor::new_ext(
2276 src.as_bytes(),
2277 Format::Markdown,
2278 MarkdownExtensions { directives: true, ..Default::default() },
2279 )
2280 .expect("editor");
2281 assert_eq!(ext.query("directive").expect("query").len(), 1);
2282 }
2283
2284 #[test]
2285 fn document_html_elements_make_embedded_img_queryable() {
2286 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
2287 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
2289 assert_eq!(plain.query("image").expect("query").len(), 0);
2290 let mut ext = Document::parse_str_with(
2292 src,
2293 Format::Markdown,
2294 MarkdownExtensions { html_elements: true, ..Default::default() },
2295 )
2296 .expect("parse");
2297 let images = ext.query("image").expect("query");
2298 assert_eq!(images.len(), 1);
2299 assert_eq!(images[0].kind, "image");
2300 }
2301
2302 #[test]
2303 fn editor_filter_public_audience_view() {
2304 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
2305 let mut ed = Editor::new_ext(
2306 src.as_bytes(),
2307 Format::Markdown,
2308 MarkdownExtensions { directives: true, ..Default::default() },
2309 )
2310 .expect("editor");
2311 ed.filter(
2313 "directive[name=vis]",
2314 Some("directive[class~=public]"),
2315 true,
2316 )
2317 .expect("filter");
2318 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
2319 }
2320
2321 #[test]
2322 fn editor_filter_rejects_a_malformed_selector() {
2323 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2324 assert_eq!(ed.filter("list >", None, false), Err(Error::InvalidArgument));
2325 }
2326
2327 #[test]
2328 fn builder_builds_and_renders_a_document() {
2329 let mut b = Builder::new().expect("builder");
2330
2331 let title = b.add_text(TextKind::Str, "Title").unwrap();
2333 let heading = b.add_heading(1).unwrap();
2334 b.set_children(heading, &[title]).unwrap();
2335
2336 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
2337 let world = b.add_text(TextKind::Str, "world").unwrap();
2338 let emph = b.add(VoidKind::Emph).unwrap();
2339 b.set_children(emph, &[world]).unwrap();
2340 let para = b.add(VoidKind::Para).unwrap();
2341 b.set_children(para, &[hello, emph]).unwrap();
2342
2343 let doc = b.add(VoidKind::Doc).unwrap();
2344 b.set_children(doc, &[heading, para]).unwrap();
2345
2346 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
2347 assert!(html.contains("<h1>Title</h1>"), "{html}");
2348 assert!(html.contains("<em>world</em>"), "{html}");
2349
2350 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
2351 assert!(md.contains("# Title"), "{md}");
2352 assert!(md.contains("*world*"), "{md}");
2353
2354 let matches = b.query(doc, "heading").unwrap();
2355 assert_eq!(matches.len(), 1);
2356 assert_eq!(matches[0].kind, "heading");
2357
2358 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
2359 assert!(json.contains("\"kind\": \"doc\""), "{json}");
2360 }
2361
2362 #[test]
2363 fn builder_element_with_attributes() {
2364 let mut b = Builder::new().expect("builder");
2365 let inner = b.add_text(TextKind::Str, "hi").unwrap();
2366 let el = b.add_element("section").unwrap();
2367 b.set_children(el, &[inner]).unwrap();
2368 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)]).unwrap();
2369
2370 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
2371 assert!(html.contains("<section"), "{html}");
2372 assert!(html.contains("class=\"note\""), "{html}");
2373 assert!(html.contains("hidden"), "{html}");
2374 }
2375
2376 #[test]
2377 fn builder_lists_round_trip_to_markdown() {
2378 let mut b = Builder::new().expect("builder");
2379
2380 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
2382 let one_para = b.add(VoidKind::Para).unwrap();
2383 b.set_children(one_para, &[one_txt]).unwrap();
2384 let one = b.add(VoidKind::ListItem).unwrap();
2385 b.set_children(one, &[one_para]).unwrap();
2386
2387 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
2388 let two_para = b.add(VoidKind::Para).unwrap();
2389 b.set_children(two_para, &[two_txt]).unwrap();
2390 let two = b.add(VoidKind::ListItem).unwrap();
2391 b.set_children(two, &[two_para]).unwrap();
2392
2393 let list = b
2394 .add_ordered_list(OrderedNumbering::Decimal, OrderedDelim::Period, true, Some(1))
2395 .unwrap();
2396 b.set_children(list, &[one, two]).unwrap();
2397 let doc = b.add(VoidKind::Doc).unwrap();
2398 b.set_children(doc, &[list]).unwrap();
2399
2400 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
2401 assert!(md.contains("1. one"), "{md}");
2402 assert!(md.contains("2. two"), "{md}");
2403 }
2404
2405 #[test]
2406 fn builder_rejects_invalid_kind_and_id() {
2407 let b = Builder::new().expect("builder");
2408 let mut id = 0u32;
2412 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
2413 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2414
2415 let mut ptr = std::ptr::null();
2417 let mut len = 0usize;
2418 let status = unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
2419 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2420 }
2421}