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)]
155pub struct Version {
156 pub major: u8,
157 pub minor: u8,
158 pub patch: u8,
159}
160
161pub fn version() -> Version {
162 let packed = unsafe { ffi::twig_version() };
163 Version {
164 major: (packed >> 16) as u8,
165 minor: (packed >> 8) as u8,
166 patch: packed as u8,
167 }
168}
169
170pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
176
177pub fn abi_version() -> u32 {
183 unsafe { ffi::twig_abi_version() }
184}
185
186pub fn version_string() -> &'static str {
187 let ptr = unsafe { ffi::twig_version_string() };
188 unsafe { std::ffi::CStr::from_ptr(ptr) }
189 .to_str()
190 .unwrap_or("")
191}
192
193#[derive(Debug)]
194pub struct Document {
195 raw: NonNull<ffi::TwigDocument>,
196}
197
198impl Document {
199 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
200 let mut raw = std::ptr::null_mut();
201 let ffi_format: ffi::TwigFormat = format.into();
202 let status = unsafe { ffi::twig_parse(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
203 Error::from_status(status)?;
204 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
205 Ok(Self { raw })
206 }
207
208 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
209 Self::parse(input.as_bytes(), format)
210 }
211
212 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
215 let raw = self.raw.as_ptr();
216 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
217 }
218
219 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
225 let raw = self.raw.as_ptr();
226 let ffi_format: ffi::TwigFormat = format.into();
227 collect_bytes(|ptr, len| unsafe {
228 ffi::twig_document_serialize(raw, ffi_format as i32, ptr, len)
229 })
230 }
231
232 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
235 let raw = self.raw.as_ptr();
236 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
237 }
238
239 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
248 let raw = self.raw.as_ptr();
249 collect_matches(|ptr, len| unsafe {
250 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
251 })
252 }
253}
254
255impl Drop for Document {
256 fn drop(&mut self) {
257 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
258 }
259}
260
261#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
265pub struct MarkdownExtensions {
266 pub directives: bool,
268 pub math: bool,
270}
271
272impl MarkdownExtensions {
273 fn to_flags(self) -> u32 {
274 let mut flags = 0;
275 if self.directives {
276 flags |= ffi::TWIG_MD_DIRECTIVES;
277 }
278 if self.math {
279 flags |= ffi::TWIG_MD_MATH;
280 }
281 flags
282 }
283}
284
285#[derive(Debug)]
291pub struct Editor {
292 raw: NonNull<ffi::TwigEditor>,
293}
294
295impl Editor {
296 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
299 let mut raw = std::ptr::null_mut();
300 let ffi_format: ffi::TwigFormat = format.into();
301 let status =
302 unsafe { ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
303 Error::from_status(status)?;
304 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
305 Ok(Self { raw })
306 }
307
308 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
309 Self::new(input.as_bytes(), format)
310 }
311
312 pub fn new_ext(input: &[u8], format: Format, extensions: MarkdownExtensions) -> Result<Self, Error> {
317 let mut raw = std::ptr::null_mut();
318 let ffi_format: ffi::TwigFormat = format.into();
319 let status = unsafe {
320 ffi::twig_editor_create_ext(
321 input.as_ptr(),
322 input.len(),
323 ffi_format as i32,
324 extensions.to_flags(),
325 &mut raw,
326 )
327 };
328 Error::from_status(status)?;
329 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
330 Ok(Self { raw })
331 }
332
333 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
335 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
336 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
337 })
338 }
339
340 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
343 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
344 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
345 })
346 }
347
348 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
350 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
351 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
352 })
353 }
354
355 pub fn insert_after(&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_insert_after(ed, loc, loc_len, txt, txt_len)
359 })
360 }
361
362 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
365 let status = unsafe {
366 ffi::twig_editor_insert_child(
367 self.raw.as_ptr(),
368 locator.as_ptr(),
369 locator.len(),
370 index,
371 text.as_ptr(),
372 text.len(),
373 )
374 };
375 Error::from_status(status)
376 }
377
378 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
381 let status = unsafe {
382 ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len())
383 };
384 Error::from_status(status)
385 }
386
387 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
390 let status = unsafe {
391 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
392 };
393 Error::from_status(status)
394 }
395
396 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
400 let status = unsafe {
401 ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len())
402 };
403 Error::from_status(status)
404 }
405
406 pub fn filter(&mut self, drop: &str, keep: Option<&str>, unwrap_kept: bool) -> Result<(), Error> {
411 let (keep_ptr, keep_len) = match keep {
412 Some(k) => (k.as_ptr(), k.len()),
413 None => (std::ptr::null(), 0),
414 };
415 let status = unsafe {
416 ffi::twig_editor_filter(
417 self.raw.as_ptr(),
418 drop.as_ptr(),
419 drop.len(),
420 keep_ptr,
421 keep_len,
422 unwrap_kept as i32,
423 )
424 };
425 Error::from_status(status)
426 }
427
428 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
430 let raw = self.raw.as_ptr();
431 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
432 }
433
434 pub fn source_str(&mut self) -> Result<String, Error> {
436 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
437 }
438
439 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
442 let raw = self.raw.as_ptr();
443 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
444 }
445
446 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
449 let raw = self.raw.as_ptr();
450 collect_matches(|ptr, len| unsafe {
451 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
452 })
453 }
454
455 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
465 let mut change = ffi::TwigChange {
466 old_span: ffi::TwigSpan { start: 0, end: 0 },
467 new_span: ffi::TwigSpan { start: 0, end: 0 },
468 };
469 let status = unsafe {
470 ffi::twig_editor_edit_range(
471 self.raw.as_ptr(),
472 start,
473 end,
474 text.as_ptr(),
475 text.len(),
476 &mut change,
477 )
478 };
479 Error::from_status(status)?;
480 Ok(Change::from_ffi(change))
481 }
482
483 pub fn last_change(&mut self) -> Option<Change> {
489 let mut change = ffi::TwigChange {
490 old_span: ffi::TwigSpan { start: 0, end: 0 },
491 new_span: ffi::TwigSpan { start: 0, end: 0 },
492 };
493 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
494 match status.0 {
495 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
496 _ => None,
497 }
498 }
499
500 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
505 let mut change = ffi::TwigChange {
506 old_span: ffi::TwigSpan { start: 0, end: 0 },
507 new_span: ffi::TwigSpan { start: 0, end: 0 },
508 };
509 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
510 if status.0 == ffi::TwigStatus::NOT_FOUND {
511 return Ok(None);
512 }
513 Error::from_status(status)?;
514 Ok(Some(Change::from_ffi(change)))
515 }
516
517 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
521 let mut change = ffi::TwigChange {
522 old_span: ffi::TwigSpan { start: 0, end: 0 },
523 new_span: ffi::TwigSpan { start: 0, end: 0 },
524 };
525 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
526 if status.0 == ffi::TwigStatus::NOT_FOUND {
527 return Ok(None);
528 }
529 Error::from_status(status)?;
530 Ok(Some(Change::from_ffi(change)))
531 }
532
533 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
538 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
539 Error::from_status(status)
540 }
541
542 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
547 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
548 let mut len = 0usize;
549 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
550 Error::from_status(status)?;
551 if len == 0 {
552 return Ok(Vec::new());
553 }
554 if ptr.is_null() {
555 return Err(Error::Internal);
556 }
557 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
558 raw.iter().map(flat_node_from_ffi).collect()
559 }
560
561 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
566 let mut m = ffi::TwigQueryMatch {
567 node_id: 0,
568 span: ffi::TwigSpan { start: 0, end: 0 },
569 content_span: ffi::TwigSpan { start: 0, end: 0 },
570 has_content_span: 0,
571 kind: std::ptr::null(),
572 };
573 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
574 match status.0 {
575 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
576 ffi::TwigStatus::NOT_FOUND => Ok(None),
577 _ => Err(Error::from_status(status).unwrap_err()),
578 }
579 }
580
581 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
585 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
586 let mut len = 0usize;
587 let status =
588 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
589 match status.0 {
590 ffi::TwigStatus::OK => {}
591 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
592 _ => return Err(Error::from_status(status).unwrap_err()),
593 }
594 if len == 0 || ptr.is_null() {
595 return Ok(Vec::new());
596 }
597 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
598 raw.iter().map(query_match_from_ffi).collect()
599 }
600
601 pub fn wrap_range(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
609 self.change_op(|ed, out| unsafe {
610 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
611 })
612 }
613
614 pub fn toggle_inline(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
619 self.change_op(|ed, out| unsafe {
620 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
621 })
622 }
623
624 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
631 let (block_kind, level) = kind.to_c();
632 self.change_op(|ed, out| unsafe {
633 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
634 })
635 }
636
637 fn change_op(
640 &mut self,
641 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
642 ) -> Result<Change, Error> {
643 let mut change = ffi::TwigChange {
644 old_span: ffi::TwigSpan { start: 0, end: 0 },
645 new_span: ffi::TwigSpan { start: 0, end: 0 },
646 };
647 let status = op(self.raw.as_ptr(), &mut change);
648 Error::from_status(status)?;
649 Ok(Change::from_ffi(change))
650 }
651
652 fn apply(
654 &mut self,
655 locator: &str,
656 text: &str,
657 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
658 ) -> Result<(), Error> {
659 let status = op(
660 self.raw.as_ptr(),
661 locator.as_ptr(),
662 locator.len(),
663 text.as_ptr(),
664 text.len(),
665 );
666 Error::from_status(status)
667 }
668}
669
670impl Drop for Editor {
671 fn drop(&mut self) {
672 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
673 }
674}
675
676fn collect_bytes(
681 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
682) -> Result<Vec<u8>, Error> {
683 let mut ptr = std::ptr::null();
684 let mut len = 0usize;
685 let status = call(&mut ptr, &mut len);
686 Error::from_status(status)?;
687 if len == 0 {
688 return Ok(Vec::new());
689 }
690 if ptr.is_null() {
691 return Err(Error::Internal);
692 }
693 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
694 Ok(bytes.to_vec())
695}
696
697fn collect_matches(
700 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
701) -> Result<Vec<QueryMatch>, Error> {
702 let mut ptr = std::ptr::null();
703 let mut len = 0usize;
704 let status = call(&mut ptr, &mut len);
705 Error::from_status(status)?;
706 if len == 0 {
707 return Ok(Vec::new());
708 }
709 if ptr.is_null() {
710 return Err(Error::Internal);
711 }
712 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
713 matches.iter().map(query_match_from_ffi).collect()
714}
715
716fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
719 Ok(QueryMatch {
720 node_id: m.node_id,
721 span: m.span.start..m.span.end,
722 content_span: if m.has_content_span != 0 {
723 Some(m.content_span.start..m.content_span.end)
724 } else {
725 None
726 },
727 kind: borrowed_cstr(m.kind)?,
728 })
729}
730
731fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
733 let node_id = |v: u32| if v == ffi::TWIG_NO_NODE { None } else { Some(NodeId(v)) };
734 Ok(FlatNode {
735 id: NodeId(n.id),
736 parent: node_id(n.parent),
737 first_child: node_id(n.first_child),
738 next_sibling: node_id(n.next_sibling),
739 span: n.span.start..n.span.end,
740 content_span: if n.has_content_span != 0 {
741 Some(n.content_span.start..n.content_span.end)
742 } else {
743 None
744 },
745 level: if n.level != 0 { Some(n.level) } else { None },
746 kind: borrowed_cstr(n.kind)?,
747 text: borrowed_bytes(n.text_ptr, n.text_len),
748 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
749 head: match n.head {
750 ffi::TWIG_HEAD_NONE => None,
751 v => Some(v != 0),
752 },
753 alignment: Alignment::from_c(n.alignment),
754 })
755}
756
757fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
759 if ptr.is_null() {
760 return Err(Error::Internal);
761 }
762 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
763 .to_str()
764 .map_err(|_| Error::Internal)?
765 .to_owned())
766}
767
768fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
772 if ptr.is_null() {
773 return None;
774 }
775 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
776 Some(String::from_utf8_lossy(bytes).into_owned())
777}
778
779#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
783pub struct NodeId(pub u32);
784
785#[derive(Clone, Copy, Debug, Eq, PartialEq)]
788pub enum VoidKind {
789 Doc,
790 Para,
791 ThematicBreak,
792 Section,
793 Div,
794 BlockQuote,
795 DefinitionList,
796 Table,
797 ListItem,
798 DefinitionListItem,
799 Term,
800 Definition,
801 Caption,
802 SoftBreak,
803 HardBreak,
804 NonBreakingSpace,
805 Emph,
806 Strong,
807 Span,
808 Mark,
809 Superscript,
810 Subscript,
811 Insert,
812 Delete,
813 DoubleQuoted,
814 SingleQuoted,
815}
816
817impl VoidKind {
818 fn to_c(self) -> c_int {
819 match self {
821 VoidKind::Doc => 0,
822 VoidKind::Para => 1,
823 VoidKind::ThematicBreak => 3,
824 VoidKind::Section => 4,
825 VoidKind::Div => 5,
826 VoidKind::BlockQuote => 9,
827 VoidKind::DefinitionList => 13,
828 VoidKind::Table => 14,
829 VoidKind::ListItem => 15,
830 VoidKind::DefinitionListItem => 17,
831 VoidKind::Term => 18,
832 VoidKind::Definition => 19,
833 VoidKind::Caption => 22,
834 VoidKind::SoftBreak => 26,
835 VoidKind::HardBreak => 27,
836 VoidKind::NonBreakingSpace => 28,
837 VoidKind::Emph => 38,
838 VoidKind::Strong => 39,
839 VoidKind::Span => 42,
840 VoidKind::Mark => 43,
841 VoidKind::Superscript => 44,
842 VoidKind::Subscript => 45,
843 VoidKind::Insert => 46,
844 VoidKind::Delete => 47,
845 VoidKind::DoubleQuoted => 48,
846 VoidKind::SingleQuoted => 49,
847 }
848 }
849}
850
851#[derive(Clone, Copy, Debug, Eq, PartialEq)]
853pub enum TextKind {
854 Str,
855 Symb,
856 Verbatim,
857 InlineMath,
858 DisplayMath,
859 Url,
860 Email,
861 FootnoteReference,
862 Comment,
863 Doctype,
864 Cdata,
865}
866
867impl TextKind {
868 fn to_c(self) -> c_int {
869 match self {
870 TextKind::Str => 25,
871 TextKind::Symb => 29,
872 TextKind::Verbatim => 30,
873 TextKind::InlineMath => 32,
874 TextKind::DisplayMath => 33,
875 TextKind::Url => 34,
876 TextKind::Email => 35,
877 TextKind::FootnoteReference => 36,
878 TextKind::Comment => 52,
879 TextKind::Doctype => 53,
880 TextKind::Cdata => 55,
881 }
882 }
883}
884
885#[derive(Clone, Copy, Debug, Eq, PartialEq)]
887pub enum BulletStyle {
888 Dash,
889 Plus,
890 Star,
891}
892
893impl BulletStyle {
894 fn to_c(self) -> c_int {
895 match self {
896 BulletStyle::Dash => 0,
897 BulletStyle::Plus => 1,
898 BulletStyle::Star => 2,
899 }
900 }
901}
902
903#[derive(Clone, Copy, Debug, Eq, PartialEq)]
905pub enum OrderedNumbering {
906 Decimal,
907 LowerAlpha,
908 UpperAlpha,
909 LowerRoman,
910 UpperRoman,
911}
912
913impl OrderedNumbering {
914 fn to_c(self) -> c_int {
915 match self {
916 OrderedNumbering::Decimal => 0,
917 OrderedNumbering::LowerAlpha => 1,
918 OrderedNumbering::UpperAlpha => 2,
919 OrderedNumbering::LowerRoman => 3,
920 OrderedNumbering::UpperRoman => 4,
921 }
922 }
923}
924
925#[derive(Clone, Copy, Debug, Eq, PartialEq)]
927pub enum OrderedDelim {
928 Period,
929 ParenAfter,
930 ParenBoth,
931}
932
933impl OrderedDelim {
934 fn to_c(self) -> c_int {
935 match self {
936 OrderedDelim::Period => 0,
937 OrderedDelim::ParenAfter => 1,
938 OrderedDelim::ParenBoth => 2,
939 }
940 }
941}
942
943#[derive(Clone, Copy, Debug, Eq, PartialEq)]
946pub enum Alignment {
947 Default,
948 Left,
949 Right,
950 Center,
951}
952
953impl Alignment {
954 fn to_c(self) -> c_int {
955 match self {
956 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
957 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
958 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
959 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
960 }
961 }
962
963 fn from_c(v: c_int) -> Option<Self> {
966 match v {
967 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
968 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
969 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
970 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
971 _ => None,
972 }
973 }
974}
975
976#[derive(Clone, Copy, Debug, Eq, PartialEq)]
978pub enum SmartPunctuation {
979 LeftSingleQuote,
980 RightSingleQuote,
981 LeftDoubleQuote,
982 RightDoubleQuote,
983 Ellipses,
984 EmDash,
985 EnDash,
986}
987
988impl SmartPunctuation {
989 fn to_c(self) -> c_int {
990 match self {
991 SmartPunctuation::LeftSingleQuote => 0,
992 SmartPunctuation::RightSingleQuote => 1,
993 SmartPunctuation::LeftDoubleQuote => 2,
994 SmartPunctuation::RightDoubleQuote => 3,
995 SmartPunctuation::Ellipses => 4,
996 SmartPunctuation::EmDash => 5,
997 SmartPunctuation::EnDash => 6,
998 }
999 }
1000}
1001
1002#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1004pub enum DirectiveForm {
1005 Text,
1006 Leaf,
1007 Container,
1008}
1009
1010impl DirectiveForm {
1011 fn to_c(self) -> c_int {
1012 match self {
1013 DirectiveForm::Text => 0,
1014 DirectiveForm::Leaf => 1,
1015 DirectiveForm::Container => 2,
1016 }
1017 }
1018}
1019
1020fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
1024 match s {
1025 Some(x) => (x.as_ptr(), x.len(), 1),
1026 None => (std::ptr::null(), 0, 0),
1027 }
1028}
1029
1030#[derive(Debug)]
1037pub struct Builder {
1038 raw: NonNull<ffi::TwigBuilder>,
1039}
1040
1041impl Builder {
1042 pub fn new() -> Result<Self, Error> {
1044 let mut raw = std::ptr::null_mut();
1045 let status = unsafe { ffi::twig_builder_create(&mut raw) };
1046 Error::from_status(status)?;
1047 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1048 Ok(Self { raw })
1049 }
1050
1051 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
1054 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
1055 }
1056
1057 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
1059 self.emit(|b, out| unsafe { ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out) })
1060 }
1061
1062 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
1064 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
1065 }
1066
1067 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
1069 let (lp, ll, has) = opt_str(lang);
1070 self.emit(|b, out| unsafe { ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out) })
1071 }
1072
1073 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1075 self.emit(|b, out| unsafe {
1076 ffi::twig_builder_add_raw_block(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1077 })
1078 }
1079
1080 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
1082 self.emit(|b, out| unsafe {
1083 ffi::twig_builder_add_metadata(b, lang.as_ptr(), lang.len(), text.as_ptr(), text.len(), out)
1084 })
1085 }
1086
1087 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1089 self.emit(|b, out| unsafe {
1090 ffi::twig_builder_add_raw_inline(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1091 })
1092 }
1093
1094 pub fn add_smart_punctuation(&mut self, kind: SmartPunctuation, text: &str) -> Result<NodeId, Error> {
1096 self.emit(|b, out| unsafe {
1097 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
1098 })
1099 }
1100
1101 pub fn add_link(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1104 let (dp, dl, hd) = opt_str(destination);
1105 let (rp, rl, hr) = opt_str(reference);
1106 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
1107 }
1108
1109 pub fn add_image(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1111 let (dp, dl, hd) = opt_str(destination);
1112 let (rp, rl, hr) = opt_str(reference);
1113 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
1114 }
1115
1116 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
1118 self.emit(|b, out| unsafe { ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out) })
1119 }
1120
1121 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
1123 self.emit(|b, out| unsafe { ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out) })
1124 }
1125
1126 pub fn add_processing_instruction(&mut self, target: &str, data: &str) -> Result<NodeId, Error> {
1128 self.emit(|b, out| unsafe {
1129 ffi::twig_builder_add_processing_instruction(b, target.as_ptr(), target.len(), data.as_ptr(), data.len(), out)
1130 })
1131 }
1132
1133 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
1135 self.emit(|b, out| unsafe { ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out) })
1136 }
1137
1138 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
1140 self.emit(|b, out| unsafe {
1141 ffi::twig_builder_add_reference(b, label.as_ptr(), label.len(), destination.as_ptr(), destination.len(), out)
1142 })
1143 }
1144
1145 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
1147 self.emit(|b, out| unsafe { ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out) })
1148 }
1149
1150 pub fn add_ordered_list(
1152 &mut self,
1153 numbering: OrderedNumbering,
1154 delim: OrderedDelim,
1155 tight: bool,
1156 start: Option<u32>,
1157 ) -> Result<NodeId, Error> {
1158 let (start_val, has_start) = match start {
1159 Some(s) => (s, 1),
1160 None => (0, 0),
1161 };
1162 self.emit(|b, out| unsafe {
1163 ffi::twig_builder_add_ordered_list(b, numbering.to_c(), delim.to_c(), tight as c_int, start_val, has_start, out)
1164 })
1165 }
1166
1167 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
1169 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
1170 }
1171
1172 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
1174 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list_item(b, checked as c_int, out) })
1175 }
1176
1177 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
1179 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
1180 }
1181
1182 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
1184 self.emit(|b, out| unsafe { ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out) })
1185 }
1186
1187 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
1190 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
1191 let status = unsafe { ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len()) };
1192 Error::from_status(status)
1193 }
1194
1195 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
1199 let kvs: Vec<ffi::TwigKeyVal> = attrs
1200 .iter()
1201 .map(|(k, v)| ffi::TwigKeyVal {
1202 key: k.as_ptr(),
1203 key_len: k.len(),
1204 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
1205 value_len: v.map_or(0, |s| s.len()),
1206 })
1207 .collect();
1208 let status = unsafe { ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len()) };
1209 Error::from_status(status)
1210 }
1211
1212 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1215 let raw = self.raw.as_ptr();
1216 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
1217 }
1218
1219 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
1223 let raw = self.raw.as_ptr();
1224 let ffi_format: ffi::TwigFormat = format.into();
1225 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_serialize(raw, root.0, ffi_format as i32, ptr, len) })
1226 }
1227
1228 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1230 let raw = self.raw.as_ptr();
1231 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
1232 }
1233
1234 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1237 let raw = self.raw.as_ptr();
1238 collect_matches(|ptr, len| unsafe {
1239 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
1240 })
1241 }
1242
1243 fn emit(
1246 &mut self,
1247 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
1248 ) -> Result<NodeId, Error> {
1249 let mut id: u32 = 0;
1250 let status = call(self.raw.as_ptr(), &mut id);
1251 Error::from_status(status)?;
1252 Ok(NodeId(id))
1253 }
1254}
1255
1256impl Drop for Builder {
1257 fn drop(&mut self) {
1258 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
1259 }
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264 use super::*;
1265
1266 #[test]
1267 fn abi_version_matches() {
1268 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
1272 }
1273
1274 #[test]
1275 fn parses_and_renders_markdown_html() {
1276 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1277 let html = doc.render_html().expect("render html");
1278 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
1279 }
1280
1281 #[test]
1282 fn parses_html_input() {
1283 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
1284 let html = doc.render_html().expect("render html");
1285 assert!(String::from_utf8_lossy(&html).contains("hi"));
1286 }
1287
1288 #[test]
1289 fn serialize_round_trips_and_cross_converts() {
1290 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1291
1292 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
1293 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
1294
1295 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
1297 }
1298
1299 #[test]
1300 fn serialize_markdown_to_djot() {
1301 let mut doc =
1302 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
1303 let djot = doc.serialize(Format::Djot).expect("serialize djot");
1304 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
1305 }
1306
1307 #[test]
1308 fn ast_json_dumps_the_tree() {
1309 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
1310 let json = doc.ast_json().expect("ast json");
1311 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1312 }
1313
1314 #[test]
1315 fn query_finds_nodes_by_selector() {
1316 let source = "# One\n\n## Two\n";
1317 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1318 let matches = doc.query("heading").expect("query");
1319
1320 assert_eq!(matches.len(), 2);
1321 for m in &matches {
1322 assert_eq!(m.kind, "heading");
1323 assert!(m.span.start < m.span.end);
1324 }
1325 }
1326
1327 #[test]
1328 fn query_recovers_code_spans() {
1329 let source = "prose `code` more prose\n";
1330 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1331 let matches = doc.query("verbatim").expect("query");
1332
1333 assert_eq!(matches.len(), 1);
1334 assert_eq!(&source[matches[0].span.clone()], "`code`");
1335 }
1336
1337 #[test]
1338 fn query_rejects_a_malformed_selector() {
1339 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
1340 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
1341 }
1342
1343 #[test]
1344 fn editor_edits_by_index_path() {
1345 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
1346 ed.replace_content("0.0", "bye").expect("replace_content");
1347 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
1348 }
1349
1350 #[test]
1351 fn editor_insert_child_and_delete() {
1352 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
1353 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1354 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
1355 ed.delete("0.1").expect("delete");
1356 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
1357 }
1358
1359 #[test]
1360 fn editor_edits_by_selector() {
1361 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1362 ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1363 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
1364 }
1365
1366 #[test]
1367 fn editor_locator_errors_are_distinct() {
1368 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
1369 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
1370 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
1371 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
1372 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
1374 }
1375
1376 #[test]
1377 fn editor_reparse_break_rolls_back() {
1378 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
1379 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
1380 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
1381 }
1382
1383 #[test]
1384 fn editor_leaf_content_is_not_editable() {
1385 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1386 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
1387 }
1388
1389 #[test]
1390 fn editor_query_reflects_current_tree() {
1391 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
1392 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1393 assert_eq!(ed.query("element").expect("query").len(), 3);
1395 let json = ed.ast_json().expect("ast_json");
1396 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1397 }
1398
1399 #[test]
1402 fn editor_edit_range_types_backspaces_and_reports_change() {
1403 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
1404
1405 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
1407 assert_eq!(ed.source_str().unwrap(), "aXb\n");
1408 assert_eq!(c.old, 1..1);
1409 assert_eq!(c.new, 1..2);
1410 assert_eq!(c.delta(), 1);
1411
1412 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
1414 assert_eq!(ed.source_str().unwrap(), "ab\n");
1415 assert_eq!(c2.old, 1..2);
1416 assert_eq!(c2.new, 1..1);
1417 assert_eq!(c2.delta(), -1);
1418 }
1419
1420 #[test]
1421 fn editor_edit_range_rejects_bad_ranges() {
1422 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1423 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"); }
1427
1428 #[test]
1429 fn editor_last_change_reports_locator_ops_too() {
1430 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1431 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1434 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
1435 let c = ed.last_change().expect("a change was recorded");
1436 assert_eq!(c.old, 7..13);
1438 assert_eq!(c.new, 7..17);
1439 }
1440
1441 #[test]
1442 fn editor_nodes_is_a_walkable_flat_tree() {
1443 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1444 let nodes = ed.nodes().expect("nodes");
1445 assert!(!nodes.is_empty());
1446
1447 for (i, n) in nodes.iter().enumerate() {
1449 assert_eq!(n.id, NodeId(i as u32));
1450 }
1451 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
1453 assert_eq!(roots.len(), 1);
1454 assert_eq!(roots[0].kind, "doc");
1455
1456 let heading = nodes.iter().find(|n| n.kind == "heading").expect("a heading");
1458 assert_eq!(heading.level, Some(1));
1459 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
1460
1461 assert_eq!(heading.head, None);
1463 assert_eq!(heading.alignment, None);
1464
1465 for n in nodes.iter().filter(|n| n.parent.is_some()) {
1468 let p = &nodes[n.parent.unwrap().0 as usize];
1469 let mut kid = p.first_child;
1470 let mut seen = false;
1471 while let Some(NodeId(k)) = kid {
1472 if k == n.id.0 {
1473 seen = true;
1474 break;
1475 }
1476 kid = nodes[k as usize].next_sibling;
1477 }
1478 assert!(seen, "node {:?} not found among its parent's children", n.id);
1479 }
1480 }
1481
1482 #[test]
1483 fn flat_nodes_carry_table_head_and_alignment() {
1484 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
1488 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
1489 let nodes = ed.nodes().expect("nodes");
1490
1491 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == "row").collect();
1492 assert_eq!(rows.len(), 2, "a header row and one body row");
1493 assert_eq!(rows[0].head, Some(true), "first row is the header");
1494 assert_eq!(rows[1].head, Some(false), "second row is a body row");
1495
1496 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == "cell").collect();
1497 assert_eq!(cells.len(), 4);
1498 assert_eq!(cells[0].alignment, Some(Alignment::Left));
1500 assert_eq!(cells[1].alignment, Some(Alignment::Right));
1501 assert_eq!(cells[2].alignment, Some(Alignment::Left));
1502 assert_eq!(cells[3].alignment, Some(Alignment::Right));
1503 assert_eq!(cells[0].head, Some(true));
1505 assert_eq!(cells[2].head, Some(false));
1506
1507 let mut plain = Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
1510 let pnodes = plain.nodes().expect("nodes");
1511 let pcell = pnodes.iter().find(|n| n.kind == "cell").expect("a cell");
1512 assert_eq!(pcell.alignment, Some(Alignment::Default));
1513 }
1514
1515 #[test]
1516 fn editor_node_at_and_ancestors_hit_test_offsets() {
1517 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1518
1519 let m = ed.node_at(2).expect("node_at").expect("a node covers offset 2");
1521 assert!(m.span.contains(&2));
1522
1523 let chain = ed.ancestors_at(2).expect("ancestors_at");
1525 assert!(!chain.is_empty());
1526 assert_eq!(chain[0].kind, "doc");
1527 assert_eq!(chain.last().unwrap().node_id, m.node_id);
1528
1529 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
1531 }
1532
1533 #[test]
1536 fn editor_wrap_and_toggle_inline_round_trip() {
1537 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
1538
1539 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
1541 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
1542 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
1543
1544 ed.toggle_inline(4, 8, InlineKind::Strong).expect("toggle off");
1546 assert_eq!(ed.source_str().unwrap(), "a word b\n");
1547
1548 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
1550 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
1551 }
1552
1553 #[test]
1554 fn editor_inline_kind_support_is_format_specific() {
1555 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
1557 assert_eq!(md.wrap_range(2, 6, InlineKind::Mark), Err(Error::UnsupportedFormat));
1558
1559 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
1561 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
1562 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
1563 }
1564
1565 #[test]
1566 fn editor_toggle_strips_verbatim_without_content_span() {
1567 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
1568 ed.toggle_inline(2, 8, InlineKind::Verbatim).expect("toggle code off");
1570 assert_eq!(ed.source_str().unwrap(), "a code b\n");
1571 }
1572
1573 #[test]
1574 fn editor_set_block_switches_para_and_heading_levels() {
1575 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
1576
1577 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
1579 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
1580
1581 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
1583 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
1584
1585 ed.set_block(2, BlockKind::Paragraph).expect("to para");
1587 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
1588 }
1589
1590 #[test]
1591 fn editor_set_block_rejects_bad_level_and_format() {
1592 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1593 assert_eq!(md.set_block(0, BlockKind::Heading(9)), Err(Error::InvalidArgument));
1594
1595 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1596 assert_eq!(xml.set_block(1, BlockKind::Heading(1)), Err(Error::UnsupportedFormat));
1597 }
1598
1599 #[test]
1600 fn editor_undo_redo_round_trip() {
1601 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
1602 ed.edit_range(5, 5, "!").expect("edit");
1603 assert_eq!(ed.source_str().unwrap(), "hello!\n");
1604
1605 let change = ed.undo().expect("undo ok").expect("something to undo");
1606 assert_eq!(ed.source_str().unwrap(), "hello\n");
1607 assert_eq!(change.new.end, 5);
1608 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
1609
1610 ed.redo().expect("redo ok").expect("something to redo");
1611 assert_eq!(ed.source_str().unwrap(), "hello!\n");
1612 }
1613
1614 #[test]
1615 fn editor_coalesce_folds_a_run() {
1616 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
1617 ed.edit_range(0, 0, "a").expect("edit");
1618 ed.edit_range(1, 1, "b").expect("edit");
1619 ed.coalesce_last_undo().expect("coalesce");
1620 assert_eq!(ed.source_str().unwrap(), "ab\n");
1621 ed.undo().expect("undo ok").expect("something to undo");
1623 assert_eq!(ed.source_str().unwrap(), "\n");
1624 assert!(ed.undo().expect("undo ok").is_none());
1625 }
1626
1627 #[test]
1628 fn editor_set_block_converts_setext_heading() {
1629 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
1631 ed.set_block(0, BlockKind::Heading(1)).expect("setext to atx");
1632 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
1633 }
1634
1635 #[test]
1636 fn editor_unwrap_and_smart_delete() {
1637 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
1638 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
1640
1641 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
1642 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
1644 }
1645
1646 #[test]
1647 fn editor_directives_require_the_extension_flag() {
1648 let src = ":::vis{.public}\nhi\n:::\n";
1649 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
1652 assert_eq!(plain.query("directive").expect("query").len(), 0);
1653 let mut ext = Editor::new_ext(
1655 src.as_bytes(),
1656 Format::Markdown,
1657 MarkdownExtensions { directives: true, ..Default::default() },
1658 )
1659 .expect("editor");
1660 assert_eq!(ext.query("directive").expect("query").len(), 1);
1661 }
1662
1663 #[test]
1664 fn editor_filter_public_audience_view() {
1665 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
1666 let mut ed = Editor::new_ext(
1667 src.as_bytes(),
1668 Format::Markdown,
1669 MarkdownExtensions { directives: true, ..Default::default() },
1670 )
1671 .expect("editor");
1672 ed.filter(
1674 "directive[name=vis]",
1675 Some("directive[class~=public]"),
1676 true,
1677 )
1678 .expect("filter");
1679 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
1680 }
1681
1682 #[test]
1683 fn editor_filter_rejects_a_malformed_selector() {
1684 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1685 assert_eq!(ed.filter("list >", None, false), Err(Error::InvalidArgument));
1686 }
1687
1688 #[test]
1689 fn builder_builds_and_renders_a_document() {
1690 let mut b = Builder::new().expect("builder");
1691
1692 let title = b.add_text(TextKind::Str, "Title").unwrap();
1694 let heading = b.add_heading(1).unwrap();
1695 b.set_children(heading, &[title]).unwrap();
1696
1697 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
1698 let world = b.add_text(TextKind::Str, "world").unwrap();
1699 let emph = b.add(VoidKind::Emph).unwrap();
1700 b.set_children(emph, &[world]).unwrap();
1701 let para = b.add(VoidKind::Para).unwrap();
1702 b.set_children(para, &[hello, emph]).unwrap();
1703
1704 let doc = b.add(VoidKind::Doc).unwrap();
1705 b.set_children(doc, &[heading, para]).unwrap();
1706
1707 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
1708 assert!(html.contains("<h1>Title</h1>"), "{html}");
1709 assert!(html.contains("<em>world</em>"), "{html}");
1710
1711 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
1712 assert!(md.contains("# Title"), "{md}");
1713 assert!(md.contains("*world*"), "{md}");
1714
1715 let matches = b.query(doc, "heading").unwrap();
1716 assert_eq!(matches.len(), 1);
1717 assert_eq!(matches[0].kind, "heading");
1718
1719 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
1720 assert!(json.contains("\"kind\": \"doc\""), "{json}");
1721 }
1722
1723 #[test]
1724 fn builder_element_with_attributes() {
1725 let mut b = Builder::new().expect("builder");
1726 let inner = b.add_text(TextKind::Str, "hi").unwrap();
1727 let el = b.add_element("section").unwrap();
1728 b.set_children(el, &[inner]).unwrap();
1729 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)]).unwrap();
1730
1731 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
1732 assert!(html.contains("<section"), "{html}");
1733 assert!(html.contains("class=\"note\""), "{html}");
1734 assert!(html.contains("hidden"), "{html}");
1735 }
1736
1737 #[test]
1738 fn builder_lists_round_trip_to_markdown() {
1739 let mut b = Builder::new().expect("builder");
1740
1741 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
1743 let one_para = b.add(VoidKind::Para).unwrap();
1744 b.set_children(one_para, &[one_txt]).unwrap();
1745 let one = b.add(VoidKind::ListItem).unwrap();
1746 b.set_children(one, &[one_para]).unwrap();
1747
1748 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
1749 let two_para = b.add(VoidKind::Para).unwrap();
1750 b.set_children(two_para, &[two_txt]).unwrap();
1751 let two = b.add(VoidKind::ListItem).unwrap();
1752 b.set_children(two, &[two_para]).unwrap();
1753
1754 let list = b
1755 .add_ordered_list(OrderedNumbering::Decimal, OrderedDelim::Period, true, Some(1))
1756 .unwrap();
1757 b.set_children(list, &[one, two]).unwrap();
1758 let doc = b.add(VoidKind::Doc).unwrap();
1759 b.set_children(doc, &[list]).unwrap();
1760
1761 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
1762 assert!(md.contains("1. one"), "{md}");
1763 assert!(md.contains("2. two"), "{md}");
1764 }
1765
1766 #[test]
1767 fn builder_rejects_invalid_kind_and_id() {
1768 let b = Builder::new().expect("builder");
1769 let mut id = 0u32;
1773 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
1774 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
1775
1776 let mut ptr = std::ptr::null();
1778 let mut len = 0usize;
1779 let status = unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
1780 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
1781 }
1782}