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)]
77pub struct FlatNode {
78 pub id: NodeId,
79 pub parent: Option<NodeId>,
80 pub first_child: Option<NodeId>,
81 pub next_sibling: Option<NodeId>,
82 pub span: Range<usize>,
83 pub content_span: Option<Range<usize>>,
84 pub level: Option<u32>,
86 pub kind: String,
87 pub text: Option<String>,
88 pub destination: Option<String>,
89}
90
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub enum InlineKind {
97 Strong,
98 Emph,
99 Verbatim,
100 Mark,
101 Superscript,
102 Subscript,
103 Insert,
104 Delete,
105}
106
107impl InlineKind {
108 fn to_c(self) -> c_int {
109 match self {
110 InlineKind::Strong => 0,
111 InlineKind::Emph => 1,
112 InlineKind::Verbatim => 2,
113 InlineKind::Mark => 3,
114 InlineKind::Superscript => 4,
115 InlineKind::Subscript => 5,
116 InlineKind::Insert => 6,
117 InlineKind::Delete => 7,
118 }
119 }
120}
121
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub enum BlockKind {
125 Paragraph,
126 Heading(u32),
128}
129
130impl BlockKind {
131 fn to_c(self) -> (c_int, u32) {
133 match self {
134 BlockKind::Paragraph => (0, 0),
135 BlockKind::Heading(level) => (1, level),
136 }
137 }
138}
139
140#[derive(Clone, Copy, Debug, Eq, PartialEq)]
141pub struct Version {
142 pub major: u8,
143 pub minor: u8,
144 pub patch: u8,
145}
146
147pub fn version() -> Version {
148 let packed = unsafe { ffi::twig_version() };
149 Version {
150 major: (packed >> 16) as u8,
151 minor: (packed >> 8) as u8,
152 patch: packed as u8,
153 }
154}
155
156pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
162
163pub fn abi_version() -> u32 {
169 unsafe { ffi::twig_abi_version() }
170}
171
172pub fn version_string() -> &'static str {
173 let ptr = unsafe { ffi::twig_version_string() };
174 unsafe { std::ffi::CStr::from_ptr(ptr) }
175 .to_str()
176 .unwrap_or("")
177}
178
179#[derive(Debug)]
180pub struct Document {
181 raw: NonNull<ffi::TwigDocument>,
182}
183
184impl Document {
185 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
186 let mut raw = std::ptr::null_mut();
187 let ffi_format: ffi::TwigFormat = format.into();
188 let status = unsafe { ffi::twig_parse(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
189 Error::from_status(status)?;
190 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
191 Ok(Self { raw })
192 }
193
194 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
195 Self::parse(input.as_bytes(), format)
196 }
197
198 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
201 let raw = self.raw.as_ptr();
202 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
203 }
204
205 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
211 let raw = self.raw.as_ptr();
212 let ffi_format: ffi::TwigFormat = format.into();
213 collect_bytes(|ptr, len| unsafe {
214 ffi::twig_document_serialize(raw, ffi_format as i32, ptr, len)
215 })
216 }
217
218 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
221 let raw = self.raw.as_ptr();
222 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
223 }
224
225 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
234 let raw = self.raw.as_ptr();
235 collect_matches(|ptr, len| unsafe {
236 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
237 })
238 }
239}
240
241impl Drop for Document {
242 fn drop(&mut self) {
243 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
244 }
245}
246
247#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
251pub struct MarkdownExtensions {
252 pub directives: bool,
254 pub math: bool,
256}
257
258impl MarkdownExtensions {
259 fn to_flags(self) -> u32 {
260 let mut flags = 0;
261 if self.directives {
262 flags |= ffi::TWIG_MD_DIRECTIVES;
263 }
264 if self.math {
265 flags |= ffi::TWIG_MD_MATH;
266 }
267 flags
268 }
269}
270
271#[derive(Debug)]
277pub struct Editor {
278 raw: NonNull<ffi::TwigEditor>,
279}
280
281impl Editor {
282 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
285 let mut raw = std::ptr::null_mut();
286 let ffi_format: ffi::TwigFormat = format.into();
287 let status =
288 unsafe { ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
289 Error::from_status(status)?;
290 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
291 Ok(Self { raw })
292 }
293
294 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
295 Self::new(input.as_bytes(), format)
296 }
297
298 pub fn new_ext(input: &[u8], format: Format, extensions: MarkdownExtensions) -> Result<Self, Error> {
303 let mut raw = std::ptr::null_mut();
304 let ffi_format: ffi::TwigFormat = format.into();
305 let status = unsafe {
306 ffi::twig_editor_create_ext(
307 input.as_ptr(),
308 input.len(),
309 ffi_format as i32,
310 extensions.to_flags(),
311 &mut raw,
312 )
313 };
314 Error::from_status(status)?;
315 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
316 Ok(Self { raw })
317 }
318
319 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
321 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
322 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
323 })
324 }
325
326 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
329 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
330 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
331 })
332 }
333
334 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
336 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
337 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
338 })
339 }
340
341 pub fn insert_after(&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_insert_after(ed, loc, loc_len, txt, txt_len)
345 })
346 }
347
348 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
351 let status = unsafe {
352 ffi::twig_editor_insert_child(
353 self.raw.as_ptr(),
354 locator.as_ptr(),
355 locator.len(),
356 index,
357 text.as_ptr(),
358 text.len(),
359 )
360 };
361 Error::from_status(status)
362 }
363
364 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
367 let status = unsafe {
368 ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len())
369 };
370 Error::from_status(status)
371 }
372
373 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
376 let status = unsafe {
377 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
378 };
379 Error::from_status(status)
380 }
381
382 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
386 let status = unsafe {
387 ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len())
388 };
389 Error::from_status(status)
390 }
391
392 pub fn filter(&mut self, drop: &str, keep: Option<&str>, unwrap_kept: bool) -> Result<(), Error> {
397 let (keep_ptr, keep_len) = match keep {
398 Some(k) => (k.as_ptr(), k.len()),
399 None => (std::ptr::null(), 0),
400 };
401 let status = unsafe {
402 ffi::twig_editor_filter(
403 self.raw.as_ptr(),
404 drop.as_ptr(),
405 drop.len(),
406 keep_ptr,
407 keep_len,
408 unwrap_kept as i32,
409 )
410 };
411 Error::from_status(status)
412 }
413
414 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
416 let raw = self.raw.as_ptr();
417 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
418 }
419
420 pub fn source_str(&mut self) -> Result<String, Error> {
422 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
423 }
424
425 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
428 let raw = self.raw.as_ptr();
429 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
430 }
431
432 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
435 let raw = self.raw.as_ptr();
436 collect_matches(|ptr, len| unsafe {
437 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
438 })
439 }
440
441 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
451 let mut change = ffi::TwigChange {
452 old_span: ffi::TwigSpan { start: 0, end: 0 },
453 new_span: ffi::TwigSpan { start: 0, end: 0 },
454 };
455 let status = unsafe {
456 ffi::twig_editor_edit_range(
457 self.raw.as_ptr(),
458 start,
459 end,
460 text.as_ptr(),
461 text.len(),
462 &mut change,
463 )
464 };
465 Error::from_status(status)?;
466 Ok(Change::from_ffi(change))
467 }
468
469 pub fn last_change(&mut self) -> Option<Change> {
475 let mut change = ffi::TwigChange {
476 old_span: ffi::TwigSpan { start: 0, end: 0 },
477 new_span: ffi::TwigSpan { start: 0, end: 0 },
478 };
479 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
480 match status.0 {
481 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
482 _ => None,
483 }
484 }
485
486 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
491 let mut change = ffi::TwigChange {
492 old_span: ffi::TwigSpan { start: 0, end: 0 },
493 new_span: ffi::TwigSpan { start: 0, end: 0 },
494 };
495 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
496 if status.0 == ffi::TwigStatus::NOT_FOUND {
497 return Ok(None);
498 }
499 Error::from_status(status)?;
500 Ok(Some(Change::from_ffi(change)))
501 }
502
503 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
507 let mut change = ffi::TwigChange {
508 old_span: ffi::TwigSpan { start: 0, end: 0 },
509 new_span: ffi::TwigSpan { start: 0, end: 0 },
510 };
511 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
512 if status.0 == ffi::TwigStatus::NOT_FOUND {
513 return Ok(None);
514 }
515 Error::from_status(status)?;
516 Ok(Some(Change::from_ffi(change)))
517 }
518
519 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
524 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
525 Error::from_status(status)
526 }
527
528 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
533 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
534 let mut len = 0usize;
535 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
536 Error::from_status(status)?;
537 if len == 0 {
538 return Ok(Vec::new());
539 }
540 if ptr.is_null() {
541 return Err(Error::Internal);
542 }
543 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
544 raw.iter().map(flat_node_from_ffi).collect()
545 }
546
547 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
552 let mut m = ffi::TwigQueryMatch {
553 node_id: 0,
554 span: ffi::TwigSpan { start: 0, end: 0 },
555 content_span: ffi::TwigSpan { start: 0, end: 0 },
556 has_content_span: 0,
557 kind: std::ptr::null(),
558 };
559 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
560 match status.0 {
561 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
562 ffi::TwigStatus::NOT_FOUND => Ok(None),
563 _ => Err(Error::from_status(status).unwrap_err()),
564 }
565 }
566
567 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
571 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
572 let mut len = 0usize;
573 let status =
574 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
575 match status.0 {
576 ffi::TwigStatus::OK => {}
577 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
578 _ => return Err(Error::from_status(status).unwrap_err()),
579 }
580 if len == 0 || ptr.is_null() {
581 return Ok(Vec::new());
582 }
583 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
584 raw.iter().map(query_match_from_ffi).collect()
585 }
586
587 pub fn wrap_range(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
595 self.change_op(|ed, out| unsafe {
596 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
597 })
598 }
599
600 pub fn toggle_inline(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
605 self.change_op(|ed, out| unsafe {
606 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
607 })
608 }
609
610 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
617 let (block_kind, level) = kind.to_c();
618 self.change_op(|ed, out| unsafe {
619 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
620 })
621 }
622
623 fn change_op(
626 &mut self,
627 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
628 ) -> Result<Change, Error> {
629 let mut change = ffi::TwigChange {
630 old_span: ffi::TwigSpan { start: 0, end: 0 },
631 new_span: ffi::TwigSpan { start: 0, end: 0 },
632 };
633 let status = op(self.raw.as_ptr(), &mut change);
634 Error::from_status(status)?;
635 Ok(Change::from_ffi(change))
636 }
637
638 fn apply(
640 &mut self,
641 locator: &str,
642 text: &str,
643 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
644 ) -> Result<(), Error> {
645 let status = op(
646 self.raw.as_ptr(),
647 locator.as_ptr(),
648 locator.len(),
649 text.as_ptr(),
650 text.len(),
651 );
652 Error::from_status(status)
653 }
654}
655
656impl Drop for Editor {
657 fn drop(&mut self) {
658 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
659 }
660}
661
662fn collect_bytes(
667 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
668) -> Result<Vec<u8>, Error> {
669 let mut ptr = std::ptr::null();
670 let mut len = 0usize;
671 let status = call(&mut ptr, &mut len);
672 Error::from_status(status)?;
673 if len == 0 {
674 return Ok(Vec::new());
675 }
676 if ptr.is_null() {
677 return Err(Error::Internal);
678 }
679 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
680 Ok(bytes.to_vec())
681}
682
683fn collect_matches(
686 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
687) -> Result<Vec<QueryMatch>, Error> {
688 let mut ptr = std::ptr::null();
689 let mut len = 0usize;
690 let status = call(&mut ptr, &mut len);
691 Error::from_status(status)?;
692 if len == 0 {
693 return Ok(Vec::new());
694 }
695 if ptr.is_null() {
696 return Err(Error::Internal);
697 }
698 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
699 matches.iter().map(query_match_from_ffi).collect()
700}
701
702fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
705 Ok(QueryMatch {
706 node_id: m.node_id,
707 span: m.span.start..m.span.end,
708 content_span: if m.has_content_span != 0 {
709 Some(m.content_span.start..m.content_span.end)
710 } else {
711 None
712 },
713 kind: borrowed_cstr(m.kind)?,
714 })
715}
716
717fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
719 let node_id = |v: u32| if v == ffi::TWIG_NO_NODE { None } else { Some(NodeId(v)) };
720 Ok(FlatNode {
721 id: NodeId(n.id),
722 parent: node_id(n.parent),
723 first_child: node_id(n.first_child),
724 next_sibling: node_id(n.next_sibling),
725 span: n.span.start..n.span.end,
726 content_span: if n.has_content_span != 0 {
727 Some(n.content_span.start..n.content_span.end)
728 } else {
729 None
730 },
731 level: if n.level != 0 { Some(n.level) } else { None },
732 kind: borrowed_cstr(n.kind)?,
733 text: borrowed_bytes(n.text_ptr, n.text_len),
734 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
735 })
736}
737
738fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
740 if ptr.is_null() {
741 return Err(Error::Internal);
742 }
743 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
744 .to_str()
745 .map_err(|_| Error::Internal)?
746 .to_owned())
747}
748
749fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
753 if ptr.is_null() {
754 return None;
755 }
756 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
757 Some(String::from_utf8_lossy(bytes).into_owned())
758}
759
760#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
764pub struct NodeId(pub u32);
765
766#[derive(Clone, Copy, Debug, Eq, PartialEq)]
769pub enum VoidKind {
770 Doc,
771 Para,
772 ThematicBreak,
773 Section,
774 Div,
775 BlockQuote,
776 DefinitionList,
777 Table,
778 ListItem,
779 DefinitionListItem,
780 Term,
781 Definition,
782 Caption,
783 SoftBreak,
784 HardBreak,
785 NonBreakingSpace,
786 Emph,
787 Strong,
788 Span,
789 Mark,
790 Superscript,
791 Subscript,
792 Insert,
793 Delete,
794 DoubleQuoted,
795 SingleQuoted,
796}
797
798impl VoidKind {
799 fn to_c(self) -> c_int {
800 match self {
802 VoidKind::Doc => 0,
803 VoidKind::Para => 1,
804 VoidKind::ThematicBreak => 3,
805 VoidKind::Section => 4,
806 VoidKind::Div => 5,
807 VoidKind::BlockQuote => 9,
808 VoidKind::DefinitionList => 13,
809 VoidKind::Table => 14,
810 VoidKind::ListItem => 15,
811 VoidKind::DefinitionListItem => 17,
812 VoidKind::Term => 18,
813 VoidKind::Definition => 19,
814 VoidKind::Caption => 22,
815 VoidKind::SoftBreak => 26,
816 VoidKind::HardBreak => 27,
817 VoidKind::NonBreakingSpace => 28,
818 VoidKind::Emph => 38,
819 VoidKind::Strong => 39,
820 VoidKind::Span => 42,
821 VoidKind::Mark => 43,
822 VoidKind::Superscript => 44,
823 VoidKind::Subscript => 45,
824 VoidKind::Insert => 46,
825 VoidKind::Delete => 47,
826 VoidKind::DoubleQuoted => 48,
827 VoidKind::SingleQuoted => 49,
828 }
829 }
830}
831
832#[derive(Clone, Copy, Debug, Eq, PartialEq)]
834pub enum TextKind {
835 Str,
836 Symb,
837 Verbatim,
838 InlineMath,
839 DisplayMath,
840 Url,
841 Email,
842 FootnoteReference,
843 Comment,
844 Doctype,
845 Cdata,
846}
847
848impl TextKind {
849 fn to_c(self) -> c_int {
850 match self {
851 TextKind::Str => 25,
852 TextKind::Symb => 29,
853 TextKind::Verbatim => 30,
854 TextKind::InlineMath => 32,
855 TextKind::DisplayMath => 33,
856 TextKind::Url => 34,
857 TextKind::Email => 35,
858 TextKind::FootnoteReference => 36,
859 TextKind::Comment => 52,
860 TextKind::Doctype => 53,
861 TextKind::Cdata => 55,
862 }
863 }
864}
865
866#[derive(Clone, Copy, Debug, Eq, PartialEq)]
868pub enum BulletStyle {
869 Dash,
870 Plus,
871 Star,
872}
873
874impl BulletStyle {
875 fn to_c(self) -> c_int {
876 match self {
877 BulletStyle::Dash => 0,
878 BulletStyle::Plus => 1,
879 BulletStyle::Star => 2,
880 }
881 }
882}
883
884#[derive(Clone, Copy, Debug, Eq, PartialEq)]
886pub enum OrderedNumbering {
887 Decimal,
888 LowerAlpha,
889 UpperAlpha,
890 LowerRoman,
891 UpperRoman,
892}
893
894impl OrderedNumbering {
895 fn to_c(self) -> c_int {
896 match self {
897 OrderedNumbering::Decimal => 0,
898 OrderedNumbering::LowerAlpha => 1,
899 OrderedNumbering::UpperAlpha => 2,
900 OrderedNumbering::LowerRoman => 3,
901 OrderedNumbering::UpperRoman => 4,
902 }
903 }
904}
905
906#[derive(Clone, Copy, Debug, Eq, PartialEq)]
908pub enum OrderedDelim {
909 Period,
910 ParenAfter,
911 ParenBoth,
912}
913
914impl OrderedDelim {
915 fn to_c(self) -> c_int {
916 match self {
917 OrderedDelim::Period => 0,
918 OrderedDelim::ParenAfter => 1,
919 OrderedDelim::ParenBoth => 2,
920 }
921 }
922}
923
924#[derive(Clone, Copy, Debug, Eq, PartialEq)]
926pub enum Alignment {
927 Default,
928 Left,
929 Right,
930 Center,
931}
932
933impl Alignment {
934 fn to_c(self) -> c_int {
935 match self {
936 Alignment::Default => 0,
937 Alignment::Left => 1,
938 Alignment::Right => 2,
939 Alignment::Center => 3,
940 }
941 }
942}
943
944#[derive(Clone, Copy, Debug, Eq, PartialEq)]
946pub enum SmartPunctuation {
947 LeftSingleQuote,
948 RightSingleQuote,
949 LeftDoubleQuote,
950 RightDoubleQuote,
951 Ellipses,
952 EmDash,
953 EnDash,
954}
955
956impl SmartPunctuation {
957 fn to_c(self) -> c_int {
958 match self {
959 SmartPunctuation::LeftSingleQuote => 0,
960 SmartPunctuation::RightSingleQuote => 1,
961 SmartPunctuation::LeftDoubleQuote => 2,
962 SmartPunctuation::RightDoubleQuote => 3,
963 SmartPunctuation::Ellipses => 4,
964 SmartPunctuation::EmDash => 5,
965 SmartPunctuation::EnDash => 6,
966 }
967 }
968}
969
970#[derive(Clone, Copy, Debug, Eq, PartialEq)]
972pub enum DirectiveForm {
973 Text,
974 Leaf,
975 Container,
976}
977
978impl DirectiveForm {
979 fn to_c(self) -> c_int {
980 match self {
981 DirectiveForm::Text => 0,
982 DirectiveForm::Leaf => 1,
983 DirectiveForm::Container => 2,
984 }
985 }
986}
987
988fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
992 match s {
993 Some(x) => (x.as_ptr(), x.len(), 1),
994 None => (std::ptr::null(), 0, 0),
995 }
996}
997
998#[derive(Debug)]
1005pub struct Builder {
1006 raw: NonNull<ffi::TwigBuilder>,
1007}
1008
1009impl Builder {
1010 pub fn new() -> Result<Self, Error> {
1012 let mut raw = std::ptr::null_mut();
1013 let status = unsafe { ffi::twig_builder_create(&mut raw) };
1014 Error::from_status(status)?;
1015 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1016 Ok(Self { raw })
1017 }
1018
1019 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
1022 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
1023 }
1024
1025 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
1027 self.emit(|b, out| unsafe { ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out) })
1028 }
1029
1030 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
1032 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
1033 }
1034
1035 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
1037 let (lp, ll, has) = opt_str(lang);
1038 self.emit(|b, out| unsafe { ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out) })
1039 }
1040
1041 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1043 self.emit(|b, out| unsafe {
1044 ffi::twig_builder_add_raw_block(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1045 })
1046 }
1047
1048 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
1050 self.emit(|b, out| unsafe {
1051 ffi::twig_builder_add_metadata(b, lang.as_ptr(), lang.len(), text.as_ptr(), text.len(), out)
1052 })
1053 }
1054
1055 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1057 self.emit(|b, out| unsafe {
1058 ffi::twig_builder_add_raw_inline(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1059 })
1060 }
1061
1062 pub fn add_smart_punctuation(&mut self, kind: SmartPunctuation, text: &str) -> Result<NodeId, Error> {
1064 self.emit(|b, out| unsafe {
1065 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
1066 })
1067 }
1068
1069 pub fn add_link(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1072 let (dp, dl, hd) = opt_str(destination);
1073 let (rp, rl, hr) = opt_str(reference);
1074 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
1075 }
1076
1077 pub fn add_image(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1079 let (dp, dl, hd) = opt_str(destination);
1080 let (rp, rl, hr) = opt_str(reference);
1081 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
1082 }
1083
1084 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
1086 self.emit(|b, out| unsafe { ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out) })
1087 }
1088
1089 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
1091 self.emit(|b, out| unsafe { ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out) })
1092 }
1093
1094 pub fn add_processing_instruction(&mut self, target: &str, data: &str) -> Result<NodeId, Error> {
1096 self.emit(|b, out| unsafe {
1097 ffi::twig_builder_add_processing_instruction(b, target.as_ptr(), target.len(), data.as_ptr(), data.len(), out)
1098 })
1099 }
1100
1101 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
1103 self.emit(|b, out| unsafe { ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out) })
1104 }
1105
1106 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
1108 self.emit(|b, out| unsafe {
1109 ffi::twig_builder_add_reference(b, label.as_ptr(), label.len(), destination.as_ptr(), destination.len(), out)
1110 })
1111 }
1112
1113 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
1115 self.emit(|b, out| unsafe { ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out) })
1116 }
1117
1118 pub fn add_ordered_list(
1120 &mut self,
1121 numbering: OrderedNumbering,
1122 delim: OrderedDelim,
1123 tight: bool,
1124 start: Option<u32>,
1125 ) -> Result<NodeId, Error> {
1126 let (start_val, has_start) = match start {
1127 Some(s) => (s, 1),
1128 None => (0, 0),
1129 };
1130 self.emit(|b, out| unsafe {
1131 ffi::twig_builder_add_ordered_list(b, numbering.to_c(), delim.to_c(), tight as c_int, start_val, has_start, out)
1132 })
1133 }
1134
1135 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
1137 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
1138 }
1139
1140 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
1142 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list_item(b, checked as c_int, out) })
1143 }
1144
1145 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
1147 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
1148 }
1149
1150 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
1152 self.emit(|b, out| unsafe { ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out) })
1153 }
1154
1155 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
1158 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
1159 let status = unsafe { ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len()) };
1160 Error::from_status(status)
1161 }
1162
1163 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
1167 let kvs: Vec<ffi::TwigKeyVal> = attrs
1168 .iter()
1169 .map(|(k, v)| ffi::TwigKeyVal {
1170 key: k.as_ptr(),
1171 key_len: k.len(),
1172 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
1173 value_len: v.map_or(0, |s| s.len()),
1174 })
1175 .collect();
1176 let status = unsafe { ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len()) };
1177 Error::from_status(status)
1178 }
1179
1180 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1183 let raw = self.raw.as_ptr();
1184 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
1185 }
1186
1187 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
1191 let raw = self.raw.as_ptr();
1192 let ffi_format: ffi::TwigFormat = format.into();
1193 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_serialize(raw, root.0, ffi_format as i32, ptr, len) })
1194 }
1195
1196 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1198 let raw = self.raw.as_ptr();
1199 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
1200 }
1201
1202 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1205 let raw = self.raw.as_ptr();
1206 collect_matches(|ptr, len| unsafe {
1207 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
1208 })
1209 }
1210
1211 fn emit(
1214 &mut self,
1215 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
1216 ) -> Result<NodeId, Error> {
1217 let mut id: u32 = 0;
1218 let status = call(self.raw.as_ptr(), &mut id);
1219 Error::from_status(status)?;
1220 Ok(NodeId(id))
1221 }
1222}
1223
1224impl Drop for Builder {
1225 fn drop(&mut self) {
1226 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
1227 }
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232 use super::*;
1233
1234 #[test]
1235 fn abi_version_matches() {
1236 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
1240 }
1241
1242 #[test]
1243 fn parses_and_renders_markdown_html() {
1244 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1245 let html = doc.render_html().expect("render html");
1246 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
1247 }
1248
1249 #[test]
1250 fn parses_html_input() {
1251 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
1252 let html = doc.render_html().expect("render html");
1253 assert!(String::from_utf8_lossy(&html).contains("hi"));
1254 }
1255
1256 #[test]
1257 fn serialize_round_trips_and_cross_converts() {
1258 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1259
1260 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
1261 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
1262
1263 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
1265 }
1266
1267 #[test]
1268 fn serialize_markdown_to_djot() {
1269 let mut doc =
1270 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
1271 let djot = doc.serialize(Format::Djot).expect("serialize djot");
1272 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
1273 }
1274
1275 #[test]
1276 fn ast_json_dumps_the_tree() {
1277 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
1278 let json = doc.ast_json().expect("ast json");
1279 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1280 }
1281
1282 #[test]
1283 fn query_finds_nodes_by_selector() {
1284 let source = "# One\n\n## Two\n";
1285 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1286 let matches = doc.query("heading").expect("query");
1287
1288 assert_eq!(matches.len(), 2);
1289 for m in &matches {
1290 assert_eq!(m.kind, "heading");
1291 assert!(m.span.start < m.span.end);
1292 }
1293 }
1294
1295 #[test]
1296 fn query_recovers_code_spans() {
1297 let source = "prose `code` more prose\n";
1298 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1299 let matches = doc.query("verbatim").expect("query");
1300
1301 assert_eq!(matches.len(), 1);
1302 assert_eq!(&source[matches[0].span.clone()], "`code`");
1303 }
1304
1305 #[test]
1306 fn query_rejects_a_malformed_selector() {
1307 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
1308 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
1309 }
1310
1311 #[test]
1312 fn editor_edits_by_index_path() {
1313 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
1314 ed.replace_content("0.0", "bye").expect("replace_content");
1315 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
1316 }
1317
1318 #[test]
1319 fn editor_insert_child_and_delete() {
1320 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
1321 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1322 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
1323 ed.delete("0.1").expect("delete");
1324 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
1325 }
1326
1327 #[test]
1328 fn editor_edits_by_selector() {
1329 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1330 ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1331 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
1332 }
1333
1334 #[test]
1335 fn editor_locator_errors_are_distinct() {
1336 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
1337 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
1338 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
1339 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
1340 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
1342 }
1343
1344 #[test]
1345 fn editor_reparse_break_rolls_back() {
1346 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
1347 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
1348 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
1349 }
1350
1351 #[test]
1352 fn editor_leaf_content_is_not_editable() {
1353 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1354 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
1355 }
1356
1357 #[test]
1358 fn editor_query_reflects_current_tree() {
1359 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
1360 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1361 assert_eq!(ed.query("element").expect("query").len(), 3);
1363 let json = ed.ast_json().expect("ast_json");
1364 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1365 }
1366
1367 #[test]
1370 fn editor_edit_range_types_backspaces_and_reports_change() {
1371 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
1372
1373 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
1375 assert_eq!(ed.source_str().unwrap(), "aXb\n");
1376 assert_eq!(c.old, 1..1);
1377 assert_eq!(c.new, 1..2);
1378 assert_eq!(c.delta(), 1);
1379
1380 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
1382 assert_eq!(ed.source_str().unwrap(), "ab\n");
1383 assert_eq!(c2.old, 1..2);
1384 assert_eq!(c2.new, 1..1);
1385 assert_eq!(c2.delta(), -1);
1386 }
1387
1388 #[test]
1389 fn editor_edit_range_rejects_bad_ranges() {
1390 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1391 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"); }
1395
1396 #[test]
1397 fn editor_last_change_reports_locator_ops_too() {
1398 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1399 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1402 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
1403 let c = ed.last_change().expect("a change was recorded");
1404 assert_eq!(c.old, 7..13);
1406 assert_eq!(c.new, 7..17);
1407 }
1408
1409 #[test]
1410 fn editor_nodes_is_a_walkable_flat_tree() {
1411 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1412 let nodes = ed.nodes().expect("nodes");
1413 assert!(!nodes.is_empty());
1414
1415 for (i, n) in nodes.iter().enumerate() {
1417 assert_eq!(n.id, NodeId(i as u32));
1418 }
1419 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
1421 assert_eq!(roots.len(), 1);
1422 assert_eq!(roots[0].kind, "doc");
1423
1424 let heading = nodes.iter().find(|n| n.kind == "heading").expect("a heading");
1426 assert_eq!(heading.level, Some(1));
1427 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
1428
1429 for n in nodes.iter().filter(|n| n.parent.is_some()) {
1432 let p = &nodes[n.parent.unwrap().0 as usize];
1433 let mut kid = p.first_child;
1434 let mut seen = false;
1435 while let Some(NodeId(k)) = kid {
1436 if k == n.id.0 {
1437 seen = true;
1438 break;
1439 }
1440 kid = nodes[k as usize].next_sibling;
1441 }
1442 assert!(seen, "node {:?} not found among its parent's children", n.id);
1443 }
1444 }
1445
1446 #[test]
1447 fn editor_node_at_and_ancestors_hit_test_offsets() {
1448 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1449
1450 let m = ed.node_at(2).expect("node_at").expect("a node covers offset 2");
1452 assert!(m.span.contains(&2));
1453
1454 let chain = ed.ancestors_at(2).expect("ancestors_at");
1456 assert!(!chain.is_empty());
1457 assert_eq!(chain[0].kind, "doc");
1458 assert_eq!(chain.last().unwrap().node_id, m.node_id);
1459
1460 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
1462 }
1463
1464 #[test]
1467 fn editor_wrap_and_toggle_inline_round_trip() {
1468 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
1469
1470 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
1472 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
1473 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
1474
1475 ed.toggle_inline(4, 8, InlineKind::Strong).expect("toggle off");
1477 assert_eq!(ed.source_str().unwrap(), "a word b\n");
1478
1479 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
1481 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
1482 }
1483
1484 #[test]
1485 fn editor_inline_kind_support_is_format_specific() {
1486 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
1488 assert_eq!(md.wrap_range(2, 6, InlineKind::Mark), Err(Error::UnsupportedFormat));
1489
1490 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
1492 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
1493 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
1494 }
1495
1496 #[test]
1497 fn editor_toggle_strips_verbatim_without_content_span() {
1498 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
1499 ed.toggle_inline(2, 8, InlineKind::Verbatim).expect("toggle code off");
1501 assert_eq!(ed.source_str().unwrap(), "a code b\n");
1502 }
1503
1504 #[test]
1505 fn editor_set_block_switches_para_and_heading_levels() {
1506 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
1507
1508 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
1510 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
1511
1512 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
1514 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
1515
1516 ed.set_block(2, BlockKind::Paragraph).expect("to para");
1518 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
1519 }
1520
1521 #[test]
1522 fn editor_set_block_rejects_bad_level_and_format() {
1523 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1524 assert_eq!(md.set_block(0, BlockKind::Heading(9)), Err(Error::InvalidArgument));
1525
1526 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1527 assert_eq!(xml.set_block(1, BlockKind::Heading(1)), Err(Error::UnsupportedFormat));
1528 }
1529
1530 #[test]
1531 fn editor_undo_redo_round_trip() {
1532 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
1533 ed.edit_range(5, 5, "!").expect("edit");
1534 assert_eq!(ed.source_str().unwrap(), "hello!\n");
1535
1536 let change = ed.undo().expect("undo ok").expect("something to undo");
1537 assert_eq!(ed.source_str().unwrap(), "hello\n");
1538 assert_eq!(change.new.end, 5);
1539 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
1540
1541 ed.redo().expect("redo ok").expect("something to redo");
1542 assert_eq!(ed.source_str().unwrap(), "hello!\n");
1543 }
1544
1545 #[test]
1546 fn editor_coalesce_folds_a_run() {
1547 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
1548 ed.edit_range(0, 0, "a").expect("edit");
1549 ed.edit_range(1, 1, "b").expect("edit");
1550 ed.coalesce_last_undo().expect("coalesce");
1551 assert_eq!(ed.source_str().unwrap(), "ab\n");
1552 ed.undo().expect("undo ok").expect("something to undo");
1554 assert_eq!(ed.source_str().unwrap(), "\n");
1555 assert!(ed.undo().expect("undo ok").is_none());
1556 }
1557
1558 #[test]
1559 fn editor_set_block_converts_setext_heading() {
1560 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
1562 ed.set_block(0, BlockKind::Heading(1)).expect("setext to atx");
1563 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
1564 }
1565
1566 #[test]
1567 fn editor_unwrap_and_smart_delete() {
1568 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
1569 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
1571
1572 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
1573 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
1575 }
1576
1577 #[test]
1578 fn editor_directives_require_the_extension_flag() {
1579 let src = ":::vis{.public}\nhi\n:::\n";
1580 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
1583 assert_eq!(plain.query("directive").expect("query").len(), 0);
1584 let mut ext = Editor::new_ext(
1586 src.as_bytes(),
1587 Format::Markdown,
1588 MarkdownExtensions { directives: true, ..Default::default() },
1589 )
1590 .expect("editor");
1591 assert_eq!(ext.query("directive").expect("query").len(), 1);
1592 }
1593
1594 #[test]
1595 fn editor_filter_public_audience_view() {
1596 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
1597 let mut ed = Editor::new_ext(
1598 src.as_bytes(),
1599 Format::Markdown,
1600 MarkdownExtensions { directives: true, ..Default::default() },
1601 )
1602 .expect("editor");
1603 ed.filter(
1605 "directive[name=vis]",
1606 Some("directive[class~=public]"),
1607 true,
1608 )
1609 .expect("filter");
1610 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
1611 }
1612
1613 #[test]
1614 fn editor_filter_rejects_a_malformed_selector() {
1615 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1616 assert_eq!(ed.filter("list >", None, false), Err(Error::InvalidArgument));
1617 }
1618
1619 #[test]
1620 fn builder_builds_and_renders_a_document() {
1621 let mut b = Builder::new().expect("builder");
1622
1623 let title = b.add_text(TextKind::Str, "Title").unwrap();
1625 let heading = b.add_heading(1).unwrap();
1626 b.set_children(heading, &[title]).unwrap();
1627
1628 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
1629 let world = b.add_text(TextKind::Str, "world").unwrap();
1630 let emph = b.add(VoidKind::Emph).unwrap();
1631 b.set_children(emph, &[world]).unwrap();
1632 let para = b.add(VoidKind::Para).unwrap();
1633 b.set_children(para, &[hello, emph]).unwrap();
1634
1635 let doc = b.add(VoidKind::Doc).unwrap();
1636 b.set_children(doc, &[heading, para]).unwrap();
1637
1638 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
1639 assert!(html.contains("<h1>Title</h1>"), "{html}");
1640 assert!(html.contains("<em>world</em>"), "{html}");
1641
1642 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
1643 assert!(md.contains("# Title"), "{md}");
1644 assert!(md.contains("*world*"), "{md}");
1645
1646 let matches = b.query(doc, "heading").unwrap();
1647 assert_eq!(matches.len(), 1);
1648 assert_eq!(matches[0].kind, "heading");
1649
1650 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
1651 assert!(json.contains("\"kind\": \"doc\""), "{json}");
1652 }
1653
1654 #[test]
1655 fn builder_element_with_attributes() {
1656 let mut b = Builder::new().expect("builder");
1657 let inner = b.add_text(TextKind::Str, "hi").unwrap();
1658 let el = b.add_element("section").unwrap();
1659 b.set_children(el, &[inner]).unwrap();
1660 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)]).unwrap();
1661
1662 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
1663 assert!(html.contains("<section"), "{html}");
1664 assert!(html.contains("class=\"note\""), "{html}");
1665 assert!(html.contains("hidden"), "{html}");
1666 }
1667
1668 #[test]
1669 fn builder_lists_round_trip_to_markdown() {
1670 let mut b = Builder::new().expect("builder");
1671
1672 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
1674 let one_para = b.add(VoidKind::Para).unwrap();
1675 b.set_children(one_para, &[one_txt]).unwrap();
1676 let one = b.add(VoidKind::ListItem).unwrap();
1677 b.set_children(one, &[one_para]).unwrap();
1678
1679 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
1680 let two_para = b.add(VoidKind::Para).unwrap();
1681 b.set_children(two_para, &[two_txt]).unwrap();
1682 let two = b.add(VoidKind::ListItem).unwrap();
1683 b.set_children(two, &[two_para]).unwrap();
1684
1685 let list = b
1686 .add_ordered_list(OrderedNumbering::Decimal, OrderedDelim::Period, true, Some(1))
1687 .unwrap();
1688 b.set_children(list, &[one, two]).unwrap();
1689 let doc = b.add(VoidKind::Doc).unwrap();
1690 b.set_children(doc, &[list]).unwrap();
1691
1692 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
1693 assert!(md.contains("1. one"), "{md}");
1694 assert!(md.contains("2. two"), "{md}");
1695 }
1696
1697 #[test]
1698 fn builder_rejects_invalid_kind_and_id() {
1699 let b = Builder::new().expect("builder");
1700 let mut id = 0u32;
1704 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
1705 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
1706
1707 let mut ptr = std::ptr::null();
1709 let mut len = 0usize;
1710 let status = unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
1711 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
1712 }
1713}