1mod error;
2mod ffi;
3
4use std::ops::Range;
5use std::os::raw::{c_char, c_int};
6use std::ptr::NonNull;
7
8pub use error::Error;
9pub use ffi::TwigSpan as Span;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum Format {
13 Djot,
14 Markdown,
15 Xml,
16 Html,
17}
18
19impl From<Format> for ffi::TwigFormat {
20 fn from(value: Format) -> Self {
21 match value {
22 Format::Djot => ffi::TwigFormat::Djot,
23 Format::Markdown => ffi::TwigFormat::Markdown,
24 Format::Xml => ffi::TwigFormat::Xml,
25 Format::Html => ffi::TwigFormat::Html,
26 }
27 }
28}
29
30#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct QueryMatch {
33 pub node_id: u32,
35 pub span: Range<usize>,
37 pub content_span: Option<Range<usize>>,
40 pub kind: String,
42}
43
44#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct Change {
52 pub old: Range<usize>,
53 pub new: Range<usize>,
54}
55
56impl Change {
57 pub fn delta(&self) -> isize {
59 self.new.len() as isize - self.old.len() as isize
60 }
61
62 fn from_ffi(c: ffi::TwigChange) -> Self {
63 Change {
64 old: c.old_span.start..c.old_span.end,
65 new: c.new_span.start..c.new_span.end,
66 }
67 }
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
81#[non_exhaustive]
82pub struct FlatNode {
83 pub id: NodeId,
84 pub parent: Option<NodeId>,
85 pub first_child: Option<NodeId>,
86 pub next_sibling: Option<NodeId>,
87 pub span: Range<usize>,
88 pub content_span: Option<Range<usize>>,
89 pub level: Option<u32>,
91 pub kind: String,
92 pub text: Option<String>,
93 pub destination: Option<String>,
94 pub head: Option<bool>,
97 pub alignment: Option<Alignment>,
103}
104
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum InlineKind {
111 Strong,
112 Emph,
113 Verbatim,
114 Mark,
115 Superscript,
116 Subscript,
117 Insert,
118 Delete,
119}
120
121impl InlineKind {
122 fn to_c(self) -> c_int {
123 match self {
124 InlineKind::Strong => 0,
125 InlineKind::Emph => 1,
126 InlineKind::Verbatim => 2,
127 InlineKind::Mark => 3,
128 InlineKind::Superscript => 4,
129 InlineKind::Subscript => 5,
130 InlineKind::Insert => 6,
131 InlineKind::Delete => 7,
132 }
133 }
134}
135
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub enum BlockKind {
139 Paragraph,
140 Heading(u32),
142}
143
144impl BlockKind {
145 fn to_c(self) -> (c_int, u32) {
147 match self {
148 BlockKind::Paragraph => (0, 0),
149 BlockKind::Heading(level) => (1, level),
150 }
151 }
152}
153
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub enum BlockContainerKind {
161 BlockQuote,
162 BulletList,
163 OrderedList,
164}
165
166impl BlockContainerKind {
167 fn to_c(self) -> c_int {
168 match self {
169 BlockContainerKind::BlockQuote => 0,
170 BlockContainerKind::BulletList => 1,
171 BlockContainerKind::OrderedList => 2,
172 }
173 }
174}
175
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177pub struct Version {
178 pub major: u8,
179 pub minor: u8,
180 pub patch: u8,
181}
182
183pub fn version() -> Version {
184 let packed = unsafe { ffi::twig_version() };
185 Version {
186 major: (packed >> 16) as u8,
187 minor: (packed >> 8) as u8,
188 patch: packed as u8,
189 }
190}
191
192pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
198
199pub fn abi_version() -> u32 {
205 unsafe { ffi::twig_abi_version() }
206}
207
208pub fn version_string() -> &'static str {
209 let ptr = unsafe { ffi::twig_version_string() };
210 unsafe { std::ffi::CStr::from_ptr(ptr) }
211 .to_str()
212 .unwrap_or("")
213}
214
215#[derive(Debug)]
216pub struct Document {
217 raw: NonNull<ffi::TwigDocument>,
218}
219
220impl Document {
221 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
222 let mut raw = std::ptr::null_mut();
223 let ffi_format: ffi::TwigFormat = format.into();
224 let status = unsafe { ffi::twig_parse(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
225 Error::from_status(status)?;
226 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
227 Ok(Self { raw })
228 }
229
230 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
231 Self::parse(input.as_bytes(), format)
232 }
233
234 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
237 let raw = self.raw.as_ptr();
238 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
239 }
240
241 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
247 let raw = self.raw.as_ptr();
248 let ffi_format: ffi::TwigFormat = format.into();
249 collect_bytes(|ptr, len| unsafe {
250 ffi::twig_document_serialize(raw, ffi_format as i32, ptr, len)
251 })
252 }
253
254 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
257 let raw = self.raw.as_ptr();
258 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
259 }
260
261 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
270 let raw = self.raw.as_ptr();
271 collect_matches(|ptr, len| unsafe {
272 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
273 })
274 }
275}
276
277impl Drop for Document {
278 fn drop(&mut self) {
279 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
280 }
281}
282
283#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
287pub struct MarkdownExtensions {
288 pub directives: bool,
290 pub math: bool,
292}
293
294impl MarkdownExtensions {
295 fn to_flags(self) -> u32 {
296 let mut flags = 0;
297 if self.directives {
298 flags |= ffi::TWIG_MD_DIRECTIVES;
299 }
300 if self.math {
301 flags |= ffi::TWIG_MD_MATH;
302 }
303 flags
304 }
305}
306
307#[derive(Debug)]
313pub struct Editor {
314 raw: NonNull<ffi::TwigEditor>,
315}
316
317impl Editor {
318 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
321 let mut raw = std::ptr::null_mut();
322 let ffi_format: ffi::TwigFormat = format.into();
323 let status =
324 unsafe { ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
325 Error::from_status(status)?;
326 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
327 Ok(Self { raw })
328 }
329
330 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
331 Self::new(input.as_bytes(), format)
332 }
333
334 pub fn new_ext(input: &[u8], format: Format, extensions: MarkdownExtensions) -> Result<Self, Error> {
339 let mut raw = std::ptr::null_mut();
340 let ffi_format: ffi::TwigFormat = format.into();
341 let status = unsafe {
342 ffi::twig_editor_create_ext(
343 input.as_ptr(),
344 input.len(),
345 ffi_format as i32,
346 extensions.to_flags(),
347 &mut raw,
348 )
349 };
350 Error::from_status(status)?;
351 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
352 Ok(Self { raw })
353 }
354
355 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
357 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
358 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
359 })
360 }
361
362 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
365 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
366 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
367 })
368 }
369
370 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
372 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
373 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
374 })
375 }
376
377 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
379 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
380 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
381 })
382 }
383
384 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
387 let status = unsafe {
388 ffi::twig_editor_insert_child(
389 self.raw.as_ptr(),
390 locator.as_ptr(),
391 locator.len(),
392 index,
393 text.as_ptr(),
394 text.len(),
395 )
396 };
397 Error::from_status(status)
398 }
399
400 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
403 let status = unsafe {
404 ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len())
405 };
406 Error::from_status(status)
407 }
408
409 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
412 let status = unsafe {
413 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
414 };
415 Error::from_status(status)
416 }
417
418 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
422 let status = unsafe {
423 ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len())
424 };
425 Error::from_status(status)
426 }
427
428 pub fn filter(&mut self, drop: &str, keep: Option<&str>, unwrap_kept: bool) -> Result<(), Error> {
433 let (keep_ptr, keep_len) = match keep {
434 Some(k) => (k.as_ptr(), k.len()),
435 None => (std::ptr::null(), 0),
436 };
437 let status = unsafe {
438 ffi::twig_editor_filter(
439 self.raw.as_ptr(),
440 drop.as_ptr(),
441 drop.len(),
442 keep_ptr,
443 keep_len,
444 unwrap_kept as i32,
445 )
446 };
447 Error::from_status(status)
448 }
449
450 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
452 let raw = self.raw.as_ptr();
453 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
454 }
455
456 pub fn source_str(&mut self) -> Result<String, Error> {
458 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
459 }
460
461 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
464 let raw = self.raw.as_ptr();
465 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
466 }
467
468 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
471 let raw = self.raw.as_ptr();
472 collect_matches(|ptr, len| unsafe {
473 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
474 })
475 }
476
477 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
487 let mut change = ffi::TwigChange {
488 old_span: ffi::TwigSpan { start: 0, end: 0 },
489 new_span: ffi::TwigSpan { start: 0, end: 0 },
490 };
491 let status = unsafe {
492 ffi::twig_editor_edit_range(
493 self.raw.as_ptr(),
494 start,
495 end,
496 text.as_ptr(),
497 text.len(),
498 &mut change,
499 )
500 };
501 Error::from_status(status)?;
502 Ok(Change::from_ffi(change))
503 }
504
505 pub fn last_change(&mut self) -> Option<Change> {
511 let mut change = ffi::TwigChange {
512 old_span: ffi::TwigSpan { start: 0, end: 0 },
513 new_span: ffi::TwigSpan { start: 0, end: 0 },
514 };
515 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
516 match status.0 {
517 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
518 _ => None,
519 }
520 }
521
522 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
527 let mut change = ffi::TwigChange {
528 old_span: ffi::TwigSpan { start: 0, end: 0 },
529 new_span: ffi::TwigSpan { start: 0, end: 0 },
530 };
531 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
532 if status.0 == ffi::TwigStatus::NOT_FOUND {
533 return Ok(None);
534 }
535 Error::from_status(status)?;
536 Ok(Some(Change::from_ffi(change)))
537 }
538
539 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
543 let mut change = ffi::TwigChange {
544 old_span: ffi::TwigSpan { start: 0, end: 0 },
545 new_span: ffi::TwigSpan { start: 0, end: 0 },
546 };
547 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
548 if status.0 == ffi::TwigStatus::NOT_FOUND {
549 return Ok(None);
550 }
551 Error::from_status(status)?;
552 Ok(Some(Change::from_ffi(change)))
553 }
554
555 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
560 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
561 Error::from_status(status)
562 }
563
564 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
569 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
570 let mut len = 0usize;
571 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
572 Error::from_status(status)?;
573 if len == 0 {
574 return Ok(Vec::new());
575 }
576 if ptr.is_null() {
577 return Err(Error::Internal);
578 }
579 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
580 raw.iter().map(flat_node_from_ffi).collect()
581 }
582
583 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
588 let mut m = ffi::TwigQueryMatch {
589 node_id: 0,
590 span: ffi::TwigSpan { start: 0, end: 0 },
591 content_span: ffi::TwigSpan { start: 0, end: 0 },
592 has_content_span: 0,
593 kind: std::ptr::null(),
594 };
595 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
596 match status.0 {
597 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
598 ffi::TwigStatus::NOT_FOUND => Ok(None),
599 _ => Err(Error::from_status(status).unwrap_err()),
600 }
601 }
602
603 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
607 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
608 let mut len = 0usize;
609 let status =
610 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
611 match status.0 {
612 ffi::TwigStatus::OK => {}
613 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
614 _ => return Err(Error::from_status(status).unwrap_err()),
615 }
616 if len == 0 || ptr.is_null() {
617 return Ok(Vec::new());
618 }
619 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
620 raw.iter().map(query_match_from_ffi).collect()
621 }
622
623 pub fn wrap_range(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
631 self.change_op(|ed, out| unsafe {
632 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
633 })
634 }
635
636 pub fn toggle_inline(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
641 self.change_op(|ed, out| unsafe {
642 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
643 })
644 }
645
646 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
653 let (block_kind, level) = kind.to_c();
654 self.change_op(|ed, out| unsafe {
655 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
656 })
657 }
658
659 pub fn toggle_block_container(
682 &mut self,
683 start: usize,
684 end: usize,
685 kind: BlockContainerKind,
686 ) -> Result<Change, Error> {
687 self.change_op(|ed, out| unsafe {
688 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
689 })
690 }
691
692 pub fn insert_link(
729 &mut self,
730 start: usize,
731 end: usize,
732 destination: &str,
733 ) -> Result<Change, Error> {
734 self.change_op(|ed, out| unsafe {
735 ffi::twig_editor_insert_link(
736 ed,
737 start,
738 end,
739 destination.as_ptr(),
740 destination.len(),
741 out,
742 )
743 })
744 }
745
746 fn change_op(
749 &mut self,
750 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
751 ) -> Result<Change, Error> {
752 let mut change = ffi::TwigChange {
753 old_span: ffi::TwigSpan { start: 0, end: 0 },
754 new_span: ffi::TwigSpan { start: 0, end: 0 },
755 };
756 let status = op(self.raw.as_ptr(), &mut change);
757 Error::from_status(status)?;
758 Ok(Change::from_ffi(change))
759 }
760
761 fn apply(
763 &mut self,
764 locator: &str,
765 text: &str,
766 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
767 ) -> Result<(), Error> {
768 let status = op(
769 self.raw.as_ptr(),
770 locator.as_ptr(),
771 locator.len(),
772 text.as_ptr(),
773 text.len(),
774 );
775 Error::from_status(status)
776 }
777}
778
779impl Drop for Editor {
780 fn drop(&mut self) {
781 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
782 }
783}
784
785fn collect_bytes(
790 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
791) -> Result<Vec<u8>, Error> {
792 let mut ptr = std::ptr::null();
793 let mut len = 0usize;
794 let status = call(&mut ptr, &mut len);
795 Error::from_status(status)?;
796 if len == 0 {
797 return Ok(Vec::new());
798 }
799 if ptr.is_null() {
800 return Err(Error::Internal);
801 }
802 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
803 Ok(bytes.to_vec())
804}
805
806fn collect_matches(
809 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
810) -> Result<Vec<QueryMatch>, Error> {
811 let mut ptr = std::ptr::null();
812 let mut len = 0usize;
813 let status = call(&mut ptr, &mut len);
814 Error::from_status(status)?;
815 if len == 0 {
816 return Ok(Vec::new());
817 }
818 if ptr.is_null() {
819 return Err(Error::Internal);
820 }
821 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
822 matches.iter().map(query_match_from_ffi).collect()
823}
824
825fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
828 Ok(QueryMatch {
829 node_id: m.node_id,
830 span: m.span.start..m.span.end,
831 content_span: if m.has_content_span != 0 {
832 Some(m.content_span.start..m.content_span.end)
833 } else {
834 None
835 },
836 kind: borrowed_cstr(m.kind)?,
837 })
838}
839
840fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
842 let node_id = |v: u32| if v == ffi::TWIG_NO_NODE { None } else { Some(NodeId(v)) };
843 Ok(FlatNode {
844 id: NodeId(n.id),
845 parent: node_id(n.parent),
846 first_child: node_id(n.first_child),
847 next_sibling: node_id(n.next_sibling),
848 span: n.span.start..n.span.end,
849 content_span: if n.has_content_span != 0 {
850 Some(n.content_span.start..n.content_span.end)
851 } else {
852 None
853 },
854 level: if n.level != 0 { Some(n.level) } else { None },
855 kind: borrowed_cstr(n.kind)?,
856 text: borrowed_bytes(n.text_ptr, n.text_len),
857 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
858 head: match n.head {
859 ffi::TWIG_HEAD_NONE => None,
860 v => Some(v != 0),
861 },
862 alignment: Alignment::from_c(n.alignment),
863 })
864}
865
866fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
868 if ptr.is_null() {
869 return Err(Error::Internal);
870 }
871 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
872 .to_str()
873 .map_err(|_| Error::Internal)?
874 .to_owned())
875}
876
877fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
881 if ptr.is_null() {
882 return None;
883 }
884 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
885 Some(String::from_utf8_lossy(bytes).into_owned())
886}
887
888#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
892pub struct NodeId(pub u32);
893
894#[derive(Clone, Copy, Debug, Eq, PartialEq)]
897pub enum VoidKind {
898 Doc,
899 Para,
900 ThematicBreak,
901 Section,
902 Div,
903 BlockQuote,
904 DefinitionList,
905 Table,
906 ListItem,
907 DefinitionListItem,
908 Term,
909 Definition,
910 Caption,
911 SoftBreak,
912 HardBreak,
913 NonBreakingSpace,
914 Emph,
915 Strong,
916 Span,
917 Mark,
918 Superscript,
919 Subscript,
920 Insert,
921 Delete,
922 DoubleQuoted,
923 SingleQuoted,
924}
925
926impl VoidKind {
927 fn to_c(self) -> c_int {
928 match self {
930 VoidKind::Doc => 0,
931 VoidKind::Para => 1,
932 VoidKind::ThematicBreak => 3,
933 VoidKind::Section => 4,
934 VoidKind::Div => 5,
935 VoidKind::BlockQuote => 9,
936 VoidKind::DefinitionList => 13,
937 VoidKind::Table => 14,
938 VoidKind::ListItem => 15,
939 VoidKind::DefinitionListItem => 17,
940 VoidKind::Term => 18,
941 VoidKind::Definition => 19,
942 VoidKind::Caption => 22,
943 VoidKind::SoftBreak => 26,
944 VoidKind::HardBreak => 27,
945 VoidKind::NonBreakingSpace => 28,
946 VoidKind::Emph => 38,
947 VoidKind::Strong => 39,
948 VoidKind::Span => 42,
949 VoidKind::Mark => 43,
950 VoidKind::Superscript => 44,
951 VoidKind::Subscript => 45,
952 VoidKind::Insert => 46,
953 VoidKind::Delete => 47,
954 VoidKind::DoubleQuoted => 48,
955 VoidKind::SingleQuoted => 49,
956 }
957 }
958}
959
960#[derive(Clone, Copy, Debug, Eq, PartialEq)]
962pub enum TextKind {
963 Str,
964 Symb,
965 Verbatim,
966 InlineMath,
967 DisplayMath,
968 Url,
969 Email,
970 FootnoteReference,
971 Comment,
972 Doctype,
973 Cdata,
974}
975
976impl TextKind {
977 fn to_c(self) -> c_int {
978 match self {
979 TextKind::Str => 25,
980 TextKind::Symb => 29,
981 TextKind::Verbatim => 30,
982 TextKind::InlineMath => 32,
983 TextKind::DisplayMath => 33,
984 TextKind::Url => 34,
985 TextKind::Email => 35,
986 TextKind::FootnoteReference => 36,
987 TextKind::Comment => 52,
988 TextKind::Doctype => 53,
989 TextKind::Cdata => 55,
990 }
991 }
992}
993
994#[derive(Clone, Copy, Debug, Eq, PartialEq)]
996pub enum BulletStyle {
997 Dash,
998 Plus,
999 Star,
1000}
1001
1002impl BulletStyle {
1003 fn to_c(self) -> c_int {
1004 match self {
1005 BulletStyle::Dash => 0,
1006 BulletStyle::Plus => 1,
1007 BulletStyle::Star => 2,
1008 }
1009 }
1010}
1011
1012#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1014pub enum OrderedNumbering {
1015 Decimal,
1016 LowerAlpha,
1017 UpperAlpha,
1018 LowerRoman,
1019 UpperRoman,
1020}
1021
1022impl OrderedNumbering {
1023 fn to_c(self) -> c_int {
1024 match self {
1025 OrderedNumbering::Decimal => 0,
1026 OrderedNumbering::LowerAlpha => 1,
1027 OrderedNumbering::UpperAlpha => 2,
1028 OrderedNumbering::LowerRoman => 3,
1029 OrderedNumbering::UpperRoman => 4,
1030 }
1031 }
1032}
1033
1034#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1036pub enum OrderedDelim {
1037 Period,
1038 ParenAfter,
1039 ParenBoth,
1040}
1041
1042impl OrderedDelim {
1043 fn to_c(self) -> c_int {
1044 match self {
1045 OrderedDelim::Period => 0,
1046 OrderedDelim::ParenAfter => 1,
1047 OrderedDelim::ParenBoth => 2,
1048 }
1049 }
1050}
1051
1052#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1055pub enum Alignment {
1056 Default,
1057 Left,
1058 Right,
1059 Center,
1060}
1061
1062impl Alignment {
1063 fn to_c(self) -> c_int {
1064 match self {
1065 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
1066 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
1067 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
1068 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
1069 }
1070 }
1071
1072 fn from_c(v: c_int) -> Option<Self> {
1075 match v {
1076 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
1077 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
1078 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
1079 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
1080 _ => None,
1081 }
1082 }
1083}
1084
1085#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1087pub enum SmartPunctuation {
1088 LeftSingleQuote,
1089 RightSingleQuote,
1090 LeftDoubleQuote,
1091 RightDoubleQuote,
1092 Ellipses,
1093 EmDash,
1094 EnDash,
1095}
1096
1097impl SmartPunctuation {
1098 fn to_c(self) -> c_int {
1099 match self {
1100 SmartPunctuation::LeftSingleQuote => 0,
1101 SmartPunctuation::RightSingleQuote => 1,
1102 SmartPunctuation::LeftDoubleQuote => 2,
1103 SmartPunctuation::RightDoubleQuote => 3,
1104 SmartPunctuation::Ellipses => 4,
1105 SmartPunctuation::EmDash => 5,
1106 SmartPunctuation::EnDash => 6,
1107 }
1108 }
1109}
1110
1111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1113pub enum DirectiveForm {
1114 Text,
1115 Leaf,
1116 Container,
1117}
1118
1119impl DirectiveForm {
1120 fn to_c(self) -> c_int {
1121 match self {
1122 DirectiveForm::Text => 0,
1123 DirectiveForm::Leaf => 1,
1124 DirectiveForm::Container => 2,
1125 }
1126 }
1127}
1128
1129fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
1133 match s {
1134 Some(x) => (x.as_ptr(), x.len(), 1),
1135 None => (std::ptr::null(), 0, 0),
1136 }
1137}
1138
1139#[derive(Debug)]
1146pub struct Builder {
1147 raw: NonNull<ffi::TwigBuilder>,
1148}
1149
1150impl Builder {
1151 pub fn new() -> Result<Self, Error> {
1153 let mut raw = std::ptr::null_mut();
1154 let status = unsafe { ffi::twig_builder_create(&mut raw) };
1155 Error::from_status(status)?;
1156 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1157 Ok(Self { raw })
1158 }
1159
1160 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
1163 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
1164 }
1165
1166 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
1168 self.emit(|b, out| unsafe { ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out) })
1169 }
1170
1171 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
1173 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
1174 }
1175
1176 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
1178 let (lp, ll, has) = opt_str(lang);
1179 self.emit(|b, out| unsafe { ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out) })
1180 }
1181
1182 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1184 self.emit(|b, out| unsafe {
1185 ffi::twig_builder_add_raw_block(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1186 })
1187 }
1188
1189 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
1191 self.emit(|b, out| unsafe {
1192 ffi::twig_builder_add_metadata(b, lang.as_ptr(), lang.len(), text.as_ptr(), text.len(), out)
1193 })
1194 }
1195
1196 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1198 self.emit(|b, out| unsafe {
1199 ffi::twig_builder_add_raw_inline(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1200 })
1201 }
1202
1203 pub fn add_smart_punctuation(&mut self, kind: SmartPunctuation, text: &str) -> Result<NodeId, Error> {
1205 self.emit(|b, out| unsafe {
1206 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
1207 })
1208 }
1209
1210 pub fn add_link(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1213 let (dp, dl, hd) = opt_str(destination);
1214 let (rp, rl, hr) = opt_str(reference);
1215 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
1216 }
1217
1218 pub fn add_image(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1220 let (dp, dl, hd) = opt_str(destination);
1221 let (rp, rl, hr) = opt_str(reference);
1222 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
1223 }
1224
1225 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
1227 self.emit(|b, out| unsafe { ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out) })
1228 }
1229
1230 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
1232 self.emit(|b, out| unsafe { ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out) })
1233 }
1234
1235 pub fn add_processing_instruction(&mut self, target: &str, data: &str) -> Result<NodeId, Error> {
1237 self.emit(|b, out| unsafe {
1238 ffi::twig_builder_add_processing_instruction(b, target.as_ptr(), target.len(), data.as_ptr(), data.len(), out)
1239 })
1240 }
1241
1242 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
1244 self.emit(|b, out| unsafe { ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out) })
1245 }
1246
1247 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
1249 self.emit(|b, out| unsafe {
1250 ffi::twig_builder_add_reference(b, label.as_ptr(), label.len(), destination.as_ptr(), destination.len(), out)
1251 })
1252 }
1253
1254 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
1256 self.emit(|b, out| unsafe { ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out) })
1257 }
1258
1259 pub fn add_ordered_list(
1261 &mut self,
1262 numbering: OrderedNumbering,
1263 delim: OrderedDelim,
1264 tight: bool,
1265 start: Option<u32>,
1266 ) -> Result<NodeId, Error> {
1267 let (start_val, has_start) = match start {
1268 Some(s) => (s, 1),
1269 None => (0, 0),
1270 };
1271 self.emit(|b, out| unsafe {
1272 ffi::twig_builder_add_ordered_list(b, numbering.to_c(), delim.to_c(), tight as c_int, start_val, has_start, out)
1273 })
1274 }
1275
1276 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
1278 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
1279 }
1280
1281 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
1283 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list_item(b, checked as c_int, out) })
1284 }
1285
1286 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
1288 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
1289 }
1290
1291 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
1293 self.emit(|b, out| unsafe { ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out) })
1294 }
1295
1296 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
1299 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
1300 let status = unsafe { ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len()) };
1301 Error::from_status(status)
1302 }
1303
1304 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
1308 let kvs: Vec<ffi::TwigKeyVal> = attrs
1309 .iter()
1310 .map(|(k, v)| ffi::TwigKeyVal {
1311 key: k.as_ptr(),
1312 key_len: k.len(),
1313 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
1314 value_len: v.map_or(0, |s| s.len()),
1315 })
1316 .collect();
1317 let status = unsafe { ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len()) };
1318 Error::from_status(status)
1319 }
1320
1321 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1324 let raw = self.raw.as_ptr();
1325 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
1326 }
1327
1328 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
1332 let raw = self.raw.as_ptr();
1333 let ffi_format: ffi::TwigFormat = format.into();
1334 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_serialize(raw, root.0, ffi_format as i32, ptr, len) })
1335 }
1336
1337 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1339 let raw = self.raw.as_ptr();
1340 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
1341 }
1342
1343 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1346 let raw = self.raw.as_ptr();
1347 collect_matches(|ptr, len| unsafe {
1348 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
1349 })
1350 }
1351
1352 fn emit(
1355 &mut self,
1356 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
1357 ) -> Result<NodeId, Error> {
1358 let mut id: u32 = 0;
1359 let status = call(self.raw.as_ptr(), &mut id);
1360 Error::from_status(status)?;
1361 Ok(NodeId(id))
1362 }
1363}
1364
1365impl Drop for Builder {
1366 fn drop(&mut self) {
1367 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
1368 }
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373 use super::*;
1374
1375 #[test]
1376 fn abi_version_matches() {
1377 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
1381 }
1382
1383 #[test]
1384 fn parses_and_renders_markdown_html() {
1385 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1386 let html = doc.render_html().expect("render html");
1387 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
1388 }
1389
1390 #[test]
1391 fn parses_html_input() {
1392 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
1393 let html = doc.render_html().expect("render html");
1394 assert!(String::from_utf8_lossy(&html).contains("hi"));
1395 }
1396
1397 #[test]
1398 fn serialize_round_trips_and_cross_converts() {
1399 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1400
1401 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
1402 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
1403
1404 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
1406 }
1407
1408 #[test]
1409 fn serialize_markdown_to_djot() {
1410 let mut doc =
1411 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
1412 let djot = doc.serialize(Format::Djot).expect("serialize djot");
1413 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
1414 }
1415
1416 #[test]
1417 fn ast_json_dumps_the_tree() {
1418 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
1419 let json = doc.ast_json().expect("ast json");
1420 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1421 }
1422
1423 #[test]
1424 fn query_finds_nodes_by_selector() {
1425 let source = "# One\n\n## Two\n";
1426 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1427 let matches = doc.query("heading").expect("query");
1428
1429 assert_eq!(matches.len(), 2);
1430 for m in &matches {
1431 assert_eq!(m.kind, "heading");
1432 assert!(m.span.start < m.span.end);
1433 }
1434 }
1435
1436 #[test]
1437 fn query_recovers_code_spans() {
1438 let source = "prose `code` more prose\n";
1439 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1440 let matches = doc.query("verbatim").expect("query");
1441
1442 assert_eq!(matches.len(), 1);
1443 assert_eq!(&source[matches[0].span.clone()], "`code`");
1444 }
1445
1446 #[test]
1447 fn query_rejects_a_malformed_selector() {
1448 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
1449 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
1450 }
1451
1452 #[test]
1453 fn editor_edits_by_index_path() {
1454 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
1455 ed.replace_content("0.0", "bye").expect("replace_content");
1456 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
1457 }
1458
1459 #[test]
1460 fn editor_insert_child_and_delete() {
1461 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
1462 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1463 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
1464 ed.delete("0.1").expect("delete");
1465 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
1466 }
1467
1468 #[test]
1469 fn editor_edits_by_selector() {
1470 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1471 ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1472 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
1473 }
1474
1475 #[test]
1476 fn editor_locator_errors_are_distinct() {
1477 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
1478 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
1479 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
1480 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
1481 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
1483 }
1484
1485 #[test]
1486 fn editor_reparse_break_rolls_back() {
1487 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
1488 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
1489 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
1490 }
1491
1492 #[test]
1493 fn editor_leaf_content_is_not_editable() {
1494 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1495 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
1496 }
1497
1498 #[test]
1499 fn editor_query_reflects_current_tree() {
1500 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
1501 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1502 assert_eq!(ed.query("element").expect("query").len(), 3);
1504 let json = ed.ast_json().expect("ast_json");
1505 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1506 }
1507
1508 #[test]
1511 fn editor_edit_range_types_backspaces_and_reports_change() {
1512 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
1513
1514 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
1516 assert_eq!(ed.source_str().unwrap(), "aXb\n");
1517 assert_eq!(c.old, 1..1);
1518 assert_eq!(c.new, 1..2);
1519 assert_eq!(c.delta(), 1);
1520
1521 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
1523 assert_eq!(ed.source_str().unwrap(), "ab\n");
1524 assert_eq!(c2.old, 1..2);
1525 assert_eq!(c2.new, 1..1);
1526 assert_eq!(c2.delta(), -1);
1527 }
1528
1529 #[test]
1530 fn editor_edit_range_rejects_bad_ranges() {
1531 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1532 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"); }
1536
1537 #[test]
1538 fn editor_last_change_reports_locator_ops_too() {
1539 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1540 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1543 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
1544 let c = ed.last_change().expect("a change was recorded");
1545 assert_eq!(c.old, 7..13);
1547 assert_eq!(c.new, 7..17);
1548 }
1549
1550 #[test]
1551 fn editor_nodes_is_a_walkable_flat_tree() {
1552 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1553 let nodes = ed.nodes().expect("nodes");
1554 assert!(!nodes.is_empty());
1555
1556 for (i, n) in nodes.iter().enumerate() {
1558 assert_eq!(n.id, NodeId(i as u32));
1559 }
1560 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
1562 assert_eq!(roots.len(), 1);
1563 assert_eq!(roots[0].kind, "doc");
1564
1565 let heading = nodes.iter().find(|n| n.kind == "heading").expect("a heading");
1567 assert_eq!(heading.level, Some(1));
1568 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
1569
1570 assert_eq!(heading.head, None);
1572 assert_eq!(heading.alignment, None);
1573
1574 for n in nodes.iter().filter(|n| n.parent.is_some()) {
1577 let p = &nodes[n.parent.unwrap().0 as usize];
1578 let mut kid = p.first_child;
1579 let mut seen = false;
1580 while let Some(NodeId(k)) = kid {
1581 if k == n.id.0 {
1582 seen = true;
1583 break;
1584 }
1585 kid = nodes[k as usize].next_sibling;
1586 }
1587 assert!(seen, "node {:?} not found among its parent's children", n.id);
1588 }
1589 }
1590
1591 #[test]
1592 fn flat_nodes_carry_table_head_and_alignment() {
1593 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
1597 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
1598 let nodes = ed.nodes().expect("nodes");
1599
1600 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == "row").collect();
1601 assert_eq!(rows.len(), 2, "a header row and one body row");
1602 assert_eq!(rows[0].head, Some(true), "first row is the header");
1603 assert_eq!(rows[1].head, Some(false), "second row is a body row");
1604
1605 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == "cell").collect();
1606 assert_eq!(cells.len(), 4);
1607 assert_eq!(cells[0].alignment, Some(Alignment::Left));
1609 assert_eq!(cells[1].alignment, Some(Alignment::Right));
1610 assert_eq!(cells[2].alignment, Some(Alignment::Left));
1611 assert_eq!(cells[3].alignment, Some(Alignment::Right));
1612 assert_eq!(cells[0].head, Some(true));
1614 assert_eq!(cells[2].head, Some(false));
1615
1616 let mut plain = Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
1619 let pnodes = plain.nodes().expect("nodes");
1620 let pcell = pnodes.iter().find(|n| n.kind == "cell").expect("a cell");
1621 assert_eq!(pcell.alignment, Some(Alignment::Default));
1622 }
1623
1624 #[test]
1625 fn editor_node_at_and_ancestors_hit_test_offsets() {
1626 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1627
1628 let m = ed.node_at(2).expect("node_at").expect("a node covers offset 2");
1630 assert!(m.span.contains(&2));
1631
1632 let chain = ed.ancestors_at(2).expect("ancestors_at");
1634 assert!(!chain.is_empty());
1635 assert_eq!(chain[0].kind, "doc");
1636 assert_eq!(chain.last().unwrap().node_id, m.node_id);
1637
1638 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
1640 }
1641
1642 #[test]
1645 fn editor_wrap_and_toggle_inline_round_trip() {
1646 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
1647
1648 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
1650 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
1651 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
1652
1653 ed.toggle_inline(4, 8, InlineKind::Strong).expect("toggle off");
1655 assert_eq!(ed.source_str().unwrap(), "a word b\n");
1656
1657 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
1659 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
1660 }
1661
1662 #[test]
1663 fn editor_inline_kind_support_is_format_specific() {
1664 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
1666 assert_eq!(md.wrap_range(2, 6, InlineKind::Mark), Err(Error::UnsupportedFormat));
1667
1668 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
1670 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
1671 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
1672 }
1673
1674 #[test]
1675 fn editor_toggle_strips_verbatim_without_content_span() {
1676 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
1677 ed.toggle_inline(2, 8, InlineKind::Verbatim).expect("toggle code off");
1679 assert_eq!(ed.source_str().unwrap(), "a code b\n");
1680 }
1681
1682 #[test]
1683 fn editor_set_block_switches_para_and_heading_levels() {
1684 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
1685
1686 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
1688 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
1689
1690 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
1692 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
1693
1694 ed.set_block(2, BlockKind::Paragraph).expect("to para");
1696 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
1697 }
1698
1699 #[test]
1700 fn editor_set_block_rejects_bad_level_and_format() {
1701 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1702 assert_eq!(md.set_block(0, BlockKind::Heading(9)), Err(Error::InvalidArgument));
1703
1704 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1705 assert_eq!(xml.set_block(1, BlockKind::Heading(1)), Err(Error::UnsupportedFormat));
1706 }
1707
1708 #[test]
1709 fn editor_toggle_block_container_round_trips() {
1710 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
1711
1712 let c = ed
1713 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
1714 .expect("quote on");
1715 assert_eq!(ed.source_str().unwrap(), "> a\n");
1716 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
1717
1718 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
1719 .expect("quote off");
1720 assert_eq!(ed.source_str().unwrap(), "a\n");
1721 }
1722
1723 #[test]
1724 fn editor_toggle_block_container_nests_a_partial_selection() {
1725 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
1726
1727 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
1730 .expect("nest");
1731 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
1732
1733 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
1735 .expect("peel");
1736 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
1737 }
1738
1739 #[test]
1740 fn editor_toggle_block_container_numbers_and_converts_lists() {
1741 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
1742
1743 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
1745 .expect("ordered on");
1746 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
1747
1748 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
1750 .expect("convert");
1751 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
1752 }
1753
1754 #[test]
1755 fn editor_toggle_block_container_rejects_unspellable_format() {
1756 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1757 assert_eq!(
1758 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
1759 Err(Error::UnsupportedFormat)
1760 );
1761 }
1762
1763 #[test]
1764 fn editor_insert_link_wraps_and_repoints() {
1765 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
1766
1767 ed.insert_link(2, 6, "http://x.dev").expect("link");
1768 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
1769
1770 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
1772 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
1773 }
1774
1775 #[test]
1776 fn editor_insert_link_repoints_an_autolink() {
1777 for format in [Format::Markdown, Format::Djot] {
1782 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
1783 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
1784 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
1785
1786 let nodes = ed.nodes().expect("nodes");
1788 let url = nodes
1789 .iter()
1790 .find(|n| n.kind == "url")
1791 .expect("still an autolink");
1792 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
1793 assert!(!nodes.iter().any(|n| n.kind == "link"));
1794 }
1795 }
1796
1797 #[test]
1798 fn editor_insert_link_escapes_the_destination() {
1799 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
1802 dj.insert_link(0, 1, "a)b").expect("link");
1803 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
1804
1805 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
1809 md.insert_link(0, 1, "a b").expect("link");
1810 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
1811
1812 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
1813 dj2.insert_link(0, 1, "a b").expect("link");
1814 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
1815 }
1816
1817 #[test]
1818 fn editor_insert_link_rejects_a_newline_destination() {
1819 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
1820 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
1821
1822 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1823 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
1824 }
1825
1826 #[test]
1827 fn editor_undo_redo_round_trip() {
1828 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
1829 ed.edit_range(5, 5, "!").expect("edit");
1830 assert_eq!(ed.source_str().unwrap(), "hello!\n");
1831
1832 let change = ed.undo().expect("undo ok").expect("something to undo");
1833 assert_eq!(ed.source_str().unwrap(), "hello\n");
1834 assert_eq!(change.new.end, 5);
1835 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
1836
1837 ed.redo().expect("redo ok").expect("something to redo");
1838 assert_eq!(ed.source_str().unwrap(), "hello!\n");
1839 }
1840
1841 #[test]
1842 fn editor_coalesce_folds_a_run() {
1843 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
1844 ed.edit_range(0, 0, "a").expect("edit");
1845 ed.edit_range(1, 1, "b").expect("edit");
1846 ed.coalesce_last_undo().expect("coalesce");
1847 assert_eq!(ed.source_str().unwrap(), "ab\n");
1848 ed.undo().expect("undo ok").expect("something to undo");
1850 assert_eq!(ed.source_str().unwrap(), "\n");
1851 assert!(ed.undo().expect("undo ok").is_none());
1852 }
1853
1854 #[test]
1855 fn editor_set_block_converts_setext_heading() {
1856 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
1858 ed.set_block(0, BlockKind::Heading(1)).expect("setext to atx");
1859 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
1860 }
1861
1862 #[test]
1863 fn editor_unwrap_and_smart_delete() {
1864 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
1865 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
1867
1868 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
1869 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
1871 }
1872
1873 #[test]
1874 fn editor_directives_require_the_extension_flag() {
1875 let src = ":::vis{.public}\nhi\n:::\n";
1876 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
1879 assert_eq!(plain.query("directive").expect("query").len(), 0);
1880 let mut ext = Editor::new_ext(
1882 src.as_bytes(),
1883 Format::Markdown,
1884 MarkdownExtensions { directives: true, ..Default::default() },
1885 )
1886 .expect("editor");
1887 assert_eq!(ext.query("directive").expect("query").len(), 1);
1888 }
1889
1890 #[test]
1891 fn editor_filter_public_audience_view() {
1892 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
1893 let mut ed = Editor::new_ext(
1894 src.as_bytes(),
1895 Format::Markdown,
1896 MarkdownExtensions { directives: true, ..Default::default() },
1897 )
1898 .expect("editor");
1899 ed.filter(
1901 "directive[name=vis]",
1902 Some("directive[class~=public]"),
1903 true,
1904 )
1905 .expect("filter");
1906 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
1907 }
1908
1909 #[test]
1910 fn editor_filter_rejects_a_malformed_selector() {
1911 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1912 assert_eq!(ed.filter("list >", None, false), Err(Error::InvalidArgument));
1913 }
1914
1915 #[test]
1916 fn builder_builds_and_renders_a_document() {
1917 let mut b = Builder::new().expect("builder");
1918
1919 let title = b.add_text(TextKind::Str, "Title").unwrap();
1921 let heading = b.add_heading(1).unwrap();
1922 b.set_children(heading, &[title]).unwrap();
1923
1924 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
1925 let world = b.add_text(TextKind::Str, "world").unwrap();
1926 let emph = b.add(VoidKind::Emph).unwrap();
1927 b.set_children(emph, &[world]).unwrap();
1928 let para = b.add(VoidKind::Para).unwrap();
1929 b.set_children(para, &[hello, emph]).unwrap();
1930
1931 let doc = b.add(VoidKind::Doc).unwrap();
1932 b.set_children(doc, &[heading, para]).unwrap();
1933
1934 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
1935 assert!(html.contains("<h1>Title</h1>"), "{html}");
1936 assert!(html.contains("<em>world</em>"), "{html}");
1937
1938 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
1939 assert!(md.contains("# Title"), "{md}");
1940 assert!(md.contains("*world*"), "{md}");
1941
1942 let matches = b.query(doc, "heading").unwrap();
1943 assert_eq!(matches.len(), 1);
1944 assert_eq!(matches[0].kind, "heading");
1945
1946 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
1947 assert!(json.contains("\"kind\": \"doc\""), "{json}");
1948 }
1949
1950 #[test]
1951 fn builder_element_with_attributes() {
1952 let mut b = Builder::new().expect("builder");
1953 let inner = b.add_text(TextKind::Str, "hi").unwrap();
1954 let el = b.add_element("section").unwrap();
1955 b.set_children(el, &[inner]).unwrap();
1956 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)]).unwrap();
1957
1958 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
1959 assert!(html.contains("<section"), "{html}");
1960 assert!(html.contains("class=\"note\""), "{html}");
1961 assert!(html.contains("hidden"), "{html}");
1962 }
1963
1964 #[test]
1965 fn builder_lists_round_trip_to_markdown() {
1966 let mut b = Builder::new().expect("builder");
1967
1968 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
1970 let one_para = b.add(VoidKind::Para).unwrap();
1971 b.set_children(one_para, &[one_txt]).unwrap();
1972 let one = b.add(VoidKind::ListItem).unwrap();
1973 b.set_children(one, &[one_para]).unwrap();
1974
1975 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
1976 let two_para = b.add(VoidKind::Para).unwrap();
1977 b.set_children(two_para, &[two_txt]).unwrap();
1978 let two = b.add(VoidKind::ListItem).unwrap();
1979 b.set_children(two, &[two_para]).unwrap();
1980
1981 let list = b
1982 .add_ordered_list(OrderedNumbering::Decimal, OrderedDelim::Period, true, Some(1))
1983 .unwrap();
1984 b.set_children(list, &[one, two]).unwrap();
1985 let doc = b.add(VoidKind::Doc).unwrap();
1986 b.set_children(doc, &[list]).unwrap();
1987
1988 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
1989 assert!(md.contains("1. one"), "{md}");
1990 assert!(md.contains("2. two"), "{md}");
1991 }
1992
1993 #[test]
1994 fn builder_rejects_invalid_kind_and_id() {
1995 let b = Builder::new().expect("builder");
1996 let mut id = 0u32;
2000 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
2001 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2002
2003 let mut ptr = std::ptr::null();
2005 let mut len = 0usize;
2006 let status = unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
2007 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2008 }
2009}