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 pub name: Option<String>,
108 pub attrs: Vec<(String, Option<String>)>,
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub enum InlineKind {
120 Strong,
121 Emph,
122 Verbatim,
123 Mark,
124 Superscript,
125 Subscript,
126 Insert,
127 Delete,
128}
129
130impl InlineKind {
131 fn to_c(self) -> c_int {
132 match self {
133 InlineKind::Strong => 0,
134 InlineKind::Emph => 1,
135 InlineKind::Verbatim => 2,
136 InlineKind::Mark => 3,
137 InlineKind::Superscript => 4,
138 InlineKind::Subscript => 5,
139 InlineKind::Insert => 6,
140 InlineKind::Delete => 7,
141 }
142 }
143}
144
145#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147pub enum BlockKind {
148 Paragraph,
149 Heading(u32),
151}
152
153impl BlockKind {
154 fn to_c(self) -> (c_int, u32) {
156 match self {
157 BlockKind::Paragraph => (0, 0),
158 BlockKind::Heading(level) => (1, level),
159 }
160 }
161}
162
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
169pub enum BlockContainerKind {
170 BlockQuote,
171 BulletList,
172 OrderedList,
173}
174
175impl BlockContainerKind {
176 fn to_c(self) -> c_int {
177 match self {
178 BlockContainerKind::BlockQuote => 0,
179 BlockContainerKind::BulletList => 1,
180 BlockContainerKind::OrderedList => 2,
181 }
182 }
183}
184
185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
186pub struct Version {
187 pub major: u8,
188 pub minor: u8,
189 pub patch: u8,
190}
191
192pub fn version() -> Version {
193 let packed = unsafe { ffi::twig_version() };
194 Version {
195 major: (packed >> 16) as u8,
196 minor: (packed >> 8) as u8,
197 patch: packed as u8,
198 }
199}
200
201pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
207
208pub fn abi_version() -> u32 {
214 unsafe { ffi::twig_abi_version() }
215}
216
217pub fn version_string() -> &'static str {
218 let ptr = unsafe { ffi::twig_version_string() };
219 unsafe { std::ffi::CStr::from_ptr(ptr) }
220 .to_str()
221 .unwrap_or("")
222}
223
224#[derive(Debug)]
225pub struct Document {
226 raw: NonNull<ffi::TwigDocument>,
227}
228
229impl Document {
230 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
231 Self::parse_with(input, format, MarkdownExtensions::default())
232 }
233
234 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
235 Self::parse(input.as_bytes(), format)
236 }
237
238 pub fn parse_with(
244 input: &[u8],
245 format: Format,
246 extensions: MarkdownExtensions,
247 ) -> Result<Self, Error> {
248 let mut raw = std::ptr::null_mut();
249 let ffi_format: ffi::TwigFormat = format.into();
250 let status = unsafe {
251 ffi::twig_parse_ext(
252 input.as_ptr(),
253 input.len(),
254 ffi_format as i32,
255 extensions.to_flags(),
256 &mut raw,
257 )
258 };
259 Error::from_status(status)?;
260 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
261 Ok(Self { raw })
262 }
263
264 pub fn parse_str_with(
266 input: &str,
267 format: Format,
268 extensions: MarkdownExtensions,
269 ) -> Result<Self, Error> {
270 Self::parse_with(input.as_bytes(), format, extensions)
271 }
272
273 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
276 let raw = self.raw.as_ptr();
277 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
278 }
279
280 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
286 let raw = self.raw.as_ptr();
287 let ffi_format: ffi::TwigFormat = format.into();
288 collect_bytes(|ptr, len| unsafe {
289 ffi::twig_document_serialize(raw, ffi_format as i32, ptr, len)
290 })
291 }
292
293 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
296 let raw = self.raw.as_ptr();
297 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
298 }
299
300 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
309 let raw = self.raw.as_ptr();
310 collect_matches(|ptr, len| unsafe {
311 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
312 })
313 }
314}
315
316impl Drop for Document {
317 fn drop(&mut self) {
318 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
319 }
320}
321
322#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
328pub struct MarkdownExtensions {
329 pub directives: bool,
331 pub math: bool,
333 pub html_elements: bool,
338}
339
340impl MarkdownExtensions {
341 fn to_flags(self) -> u32 {
342 let mut flags = 0;
343 if self.directives {
344 flags |= ffi::TWIG_MD_DIRECTIVES;
345 }
346 if self.math {
347 flags |= ffi::TWIG_MD_MATH;
348 }
349 if self.html_elements {
350 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
351 }
352 flags
353 }
354}
355
356#[derive(Debug)]
362pub struct Editor {
363 raw: NonNull<ffi::TwigEditor>,
364}
365
366impl Editor {
367 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
370 let mut raw = std::ptr::null_mut();
371 let ffi_format: ffi::TwigFormat = format.into();
372 let status =
373 unsafe { ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
374 Error::from_status(status)?;
375 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
376 Ok(Self { raw })
377 }
378
379 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
380 Self::new(input.as_bytes(), format)
381 }
382
383 pub fn new_ext(input: &[u8], format: Format, extensions: MarkdownExtensions) -> Result<Self, Error> {
388 let mut raw = std::ptr::null_mut();
389 let ffi_format: ffi::TwigFormat = format.into();
390 let status = unsafe {
391 ffi::twig_editor_create_ext(
392 input.as_ptr(),
393 input.len(),
394 ffi_format as i32,
395 extensions.to_flags(),
396 &mut raw,
397 )
398 };
399 Error::from_status(status)?;
400 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
401 Ok(Self { raw })
402 }
403
404 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
406 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
407 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
408 })
409 }
410
411 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
414 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
415 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
416 })
417 }
418
419 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
421 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
422 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
423 })
424 }
425
426 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
428 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
429 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
430 })
431 }
432
433 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
436 let status = unsafe {
437 ffi::twig_editor_insert_child(
438 self.raw.as_ptr(),
439 locator.as_ptr(),
440 locator.len(),
441 index,
442 text.as_ptr(),
443 text.len(),
444 )
445 };
446 Error::from_status(status)
447 }
448
449 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
452 let status = unsafe {
453 ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len())
454 };
455 Error::from_status(status)
456 }
457
458 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
461 let status = unsafe {
462 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
463 };
464 Error::from_status(status)
465 }
466
467 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
471 let status = unsafe {
472 ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len())
473 };
474 Error::from_status(status)
475 }
476
477 pub fn filter(&mut self, drop: &str, keep: Option<&str>, unwrap_kept: bool) -> Result<(), Error> {
482 let (keep_ptr, keep_len) = match keep {
483 Some(k) => (k.as_ptr(), k.len()),
484 None => (std::ptr::null(), 0),
485 };
486 let status = unsafe {
487 ffi::twig_editor_filter(
488 self.raw.as_ptr(),
489 drop.as_ptr(),
490 drop.len(),
491 keep_ptr,
492 keep_len,
493 unwrap_kept as i32,
494 )
495 };
496 Error::from_status(status)
497 }
498
499 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
501 let raw = self.raw.as_ptr();
502 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
503 }
504
505 pub fn source_str(&mut self) -> Result<String, Error> {
507 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
508 }
509
510 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
513 let raw = self.raw.as_ptr();
514 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
515 }
516
517 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
520 let raw = self.raw.as_ptr();
521 collect_matches(|ptr, len| unsafe {
522 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
523 })
524 }
525
526 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
536 let mut change = ffi::TwigChange {
537 old_span: ffi::TwigSpan { start: 0, end: 0 },
538 new_span: ffi::TwigSpan { start: 0, end: 0 },
539 };
540 let status = unsafe {
541 ffi::twig_editor_edit_range(
542 self.raw.as_ptr(),
543 start,
544 end,
545 text.as_ptr(),
546 text.len(),
547 &mut change,
548 )
549 };
550 Error::from_status(status)?;
551 Ok(Change::from_ffi(change))
552 }
553
554 pub fn last_change(&mut self) -> Option<Change> {
560 let mut change = ffi::TwigChange {
561 old_span: ffi::TwigSpan { start: 0, end: 0 },
562 new_span: ffi::TwigSpan { start: 0, end: 0 },
563 };
564 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
565 match status.0 {
566 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
567 _ => None,
568 }
569 }
570
571 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
576 let mut change = ffi::TwigChange {
577 old_span: ffi::TwigSpan { start: 0, end: 0 },
578 new_span: ffi::TwigSpan { start: 0, end: 0 },
579 };
580 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
581 if status.0 == ffi::TwigStatus::NOT_FOUND {
582 return Ok(None);
583 }
584 Error::from_status(status)?;
585 Ok(Some(Change::from_ffi(change)))
586 }
587
588 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
592 let mut change = ffi::TwigChange {
593 old_span: ffi::TwigSpan { start: 0, end: 0 },
594 new_span: ffi::TwigSpan { start: 0, end: 0 },
595 };
596 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
597 if status.0 == ffi::TwigStatus::NOT_FOUND {
598 return Ok(None);
599 }
600 Error::from_status(status)?;
601 Ok(Some(Change::from_ffi(change)))
602 }
603
604 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
609 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
610 Error::from_status(status)
611 }
612
613 pub fn revision(&mut self) -> u64 {
619 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
620 }
621
622 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
643 let mut span = ffi::TwigSpan { start: 0, end: 0 };
644 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
645 match status.0 {
646 ffi::TwigStatus::OK => Some(span.start..span.end),
647 _ => None,
648 }
649 }
650
651 pub fn clear_dirty(&mut self) {
656 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
657 }
658
659 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
667 let status =
668 unsafe { ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len()) };
669 Error::from_status(status)
670 }
671
672 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
677 let raw = self.raw.as_ptr();
678 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
679 }
680
681 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
686 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
687 let mut len = 0usize;
688 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
689 Error::from_status(status)?;
690 if len == 0 {
691 return Ok(Vec::new());
692 }
693 if ptr.is_null() {
694 return Err(Error::Internal);
695 }
696 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
697 raw.iter().map(flat_node_from_ffi).collect()
698 }
699
700 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
707 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
708 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
709 let mut len = 0usize;
710 let status =
711 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
712 Error::from_status(status)?;
713 if len == 0 || ptr.is_null() {
714 return Ok(Vec::new());
715 }
716 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
717 raw.iter().map(query_match_from_ffi).collect()
718 }
719
720 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
728 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
729 let mut len = 0usize;
730 let status =
731 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
732 Error::from_status(status)?;
733 if len == 0 || ptr.is_null() {
734 return Ok(Vec::new());
735 }
736 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
737 raw.iter().map(flat_node_from_ffi).collect()
738 }
739
740 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
745 let mut m = ffi::TwigQueryMatch {
746 node_id: 0,
747 span: ffi::TwigSpan { start: 0, end: 0 },
748 content_span: ffi::TwigSpan { start: 0, end: 0 },
749 has_content_span: 0,
750 kind: std::ptr::null(),
751 };
752 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
753 match status.0 {
754 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
755 ffi::TwigStatus::NOT_FOUND => Ok(None),
756 _ => Err(Error::from_status(status).unwrap_err()),
757 }
758 }
759
760 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
764 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
765 let mut len = 0usize;
766 let status =
767 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
768 match status.0 {
769 ffi::TwigStatus::OK => {}
770 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
771 _ => return Err(Error::from_status(status).unwrap_err()),
772 }
773 if len == 0 || ptr.is_null() {
774 return Ok(Vec::new());
775 }
776 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
777 raw.iter().map(query_match_from_ffi).collect()
778 }
779
780 pub fn wrap_range(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
788 self.change_op(|ed, out| unsafe {
789 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
790 })
791 }
792
793 pub fn toggle_inline(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
798 self.change_op(|ed, out| unsafe {
799 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
800 })
801 }
802
803 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
810 let (block_kind, level) = kind.to_c();
811 self.change_op(|ed, out| unsafe {
812 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
813 })
814 }
815
816 pub fn toggle_block_container(
839 &mut self,
840 start: usize,
841 end: usize,
842 kind: BlockContainerKind,
843 ) -> Result<Change, Error> {
844 self.change_op(|ed, out| unsafe {
845 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
846 })
847 }
848
849 pub fn insert_link(
894 &mut self,
895 start: usize,
896 end: usize,
897 destination: &str,
898 ) -> Result<Change, Error> {
899 self.change_op(|ed, out| unsafe {
900 ffi::twig_editor_insert_link(
901 ed,
902 start,
903 end,
904 destination.as_ptr(),
905 destination.len(),
906 out,
907 )
908 })
909 }
910
911 fn change_op(
914 &mut self,
915 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
916 ) -> Result<Change, Error> {
917 let mut change = ffi::TwigChange {
918 old_span: ffi::TwigSpan { start: 0, end: 0 },
919 new_span: ffi::TwigSpan { start: 0, end: 0 },
920 };
921 let status = op(self.raw.as_ptr(), &mut change);
922 Error::from_status(status)?;
923 Ok(Change::from_ffi(change))
924 }
925
926 fn apply(
928 &mut self,
929 locator: &str,
930 text: &str,
931 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
932 ) -> Result<(), Error> {
933 let status = op(
934 self.raw.as_ptr(),
935 locator.as_ptr(),
936 locator.len(),
937 text.as_ptr(),
938 text.len(),
939 );
940 Error::from_status(status)
941 }
942}
943
944impl Drop for Editor {
945 fn drop(&mut self) {
946 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
947 }
948}
949
950fn collect_bytes(
955 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
956) -> Result<Vec<u8>, Error> {
957 let mut ptr = std::ptr::null();
958 let mut len = 0usize;
959 let status = call(&mut ptr, &mut len);
960 Error::from_status(status)?;
961 if len == 0 {
962 return Ok(Vec::new());
963 }
964 if ptr.is_null() {
965 return Err(Error::Internal);
966 }
967 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
968 Ok(bytes.to_vec())
969}
970
971fn collect_matches(
974 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
975) -> Result<Vec<QueryMatch>, Error> {
976 let mut ptr = std::ptr::null();
977 let mut len = 0usize;
978 let status = call(&mut ptr, &mut len);
979 Error::from_status(status)?;
980 if len == 0 {
981 return Ok(Vec::new());
982 }
983 if ptr.is_null() {
984 return Err(Error::Internal);
985 }
986 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
987 matches.iter().map(query_match_from_ffi).collect()
988}
989
990fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
993 Ok(QueryMatch {
994 node_id: m.node_id,
995 span: m.span.start..m.span.end,
996 content_span: if m.has_content_span != 0 {
997 Some(m.content_span.start..m.content_span.end)
998 } else {
999 None
1000 },
1001 kind: borrowed_cstr(m.kind)?,
1002 })
1003}
1004
1005fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
1007 let node_id = |v: u32| if v == ffi::TWIG_NO_NODE { None } else { Some(NodeId(v)) };
1008 Ok(FlatNode {
1009 id: NodeId(n.id),
1010 parent: node_id(n.parent),
1011 first_child: node_id(n.first_child),
1012 next_sibling: node_id(n.next_sibling),
1013 span: n.span.start..n.span.end,
1014 content_span: if n.has_content_span != 0 {
1015 Some(n.content_span.start..n.content_span.end)
1016 } else {
1017 None
1018 },
1019 level: if n.level != 0 { Some(n.level) } else { None },
1020 kind: borrowed_cstr(n.kind)?,
1021 text: borrowed_bytes(n.text_ptr, n.text_len),
1022 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
1023 head: match n.head {
1024 ffi::TWIG_HEAD_NONE => None,
1025 v => Some(v != 0),
1026 },
1027 alignment: Alignment::from_c(n.alignment),
1028 name: borrowed_bytes(n.name_ptr, n.name_len),
1029 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
1030 })
1031}
1032
1033fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
1037 if ptr.is_null() || len == 0 {
1038 return Vec::new();
1039 }
1040 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
1041 kvs.iter()
1042 .map(|kv| {
1043 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
1044 (key, borrowed_bytes(kv.value, kv.value_len))
1045 })
1046 .collect()
1047}
1048
1049fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
1051 if ptr.is_null() {
1052 return Err(Error::Internal);
1053 }
1054 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
1055 .to_str()
1056 .map_err(|_| Error::Internal)?
1057 .to_owned())
1058}
1059
1060fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
1064 if ptr.is_null() {
1065 return None;
1066 }
1067 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1068 Some(String::from_utf8_lossy(bytes).into_owned())
1069}
1070
1071#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1075pub struct NodeId(pub u32);
1076
1077#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1080pub enum VoidKind {
1081 Doc,
1082 Para,
1083 ThematicBreak,
1084 Section,
1085 Div,
1086 BlockQuote,
1087 DefinitionList,
1088 Table,
1089 ListItem,
1090 DefinitionListItem,
1091 Term,
1092 Definition,
1093 Caption,
1094 SoftBreak,
1095 HardBreak,
1096 NonBreakingSpace,
1097 Emph,
1098 Strong,
1099 Span,
1100 Mark,
1101 Superscript,
1102 Subscript,
1103 Insert,
1104 Delete,
1105 DoubleQuoted,
1106 SingleQuoted,
1107}
1108
1109impl VoidKind {
1110 fn to_c(self) -> c_int {
1111 match self {
1113 VoidKind::Doc => 0,
1114 VoidKind::Para => 1,
1115 VoidKind::ThematicBreak => 3,
1116 VoidKind::Section => 4,
1117 VoidKind::Div => 5,
1118 VoidKind::BlockQuote => 9,
1119 VoidKind::DefinitionList => 13,
1120 VoidKind::Table => 14,
1121 VoidKind::ListItem => 15,
1122 VoidKind::DefinitionListItem => 17,
1123 VoidKind::Term => 18,
1124 VoidKind::Definition => 19,
1125 VoidKind::Caption => 22,
1126 VoidKind::SoftBreak => 26,
1127 VoidKind::HardBreak => 27,
1128 VoidKind::NonBreakingSpace => 28,
1129 VoidKind::Emph => 38,
1130 VoidKind::Strong => 39,
1131 VoidKind::Span => 42,
1132 VoidKind::Mark => 43,
1133 VoidKind::Superscript => 44,
1134 VoidKind::Subscript => 45,
1135 VoidKind::Insert => 46,
1136 VoidKind::Delete => 47,
1137 VoidKind::DoubleQuoted => 48,
1138 VoidKind::SingleQuoted => 49,
1139 }
1140 }
1141}
1142
1143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1145pub enum TextKind {
1146 Str,
1147 Symb,
1148 Verbatim,
1149 InlineMath,
1150 DisplayMath,
1151 Url,
1152 Email,
1153 FootnoteReference,
1154 Comment,
1155 Doctype,
1156 Cdata,
1157}
1158
1159impl TextKind {
1160 fn to_c(self) -> c_int {
1161 match self {
1162 TextKind::Str => 25,
1163 TextKind::Symb => 29,
1164 TextKind::Verbatim => 30,
1165 TextKind::InlineMath => 32,
1166 TextKind::DisplayMath => 33,
1167 TextKind::Url => 34,
1168 TextKind::Email => 35,
1169 TextKind::FootnoteReference => 36,
1170 TextKind::Comment => 52,
1171 TextKind::Doctype => 53,
1172 TextKind::Cdata => 55,
1173 }
1174 }
1175}
1176
1177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1179pub enum BulletStyle {
1180 Dash,
1181 Plus,
1182 Star,
1183}
1184
1185impl BulletStyle {
1186 fn to_c(self) -> c_int {
1187 match self {
1188 BulletStyle::Dash => 0,
1189 BulletStyle::Plus => 1,
1190 BulletStyle::Star => 2,
1191 }
1192 }
1193}
1194
1195#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1197pub enum OrderedNumbering {
1198 Decimal,
1199 LowerAlpha,
1200 UpperAlpha,
1201 LowerRoman,
1202 UpperRoman,
1203}
1204
1205impl OrderedNumbering {
1206 fn to_c(self) -> c_int {
1207 match self {
1208 OrderedNumbering::Decimal => 0,
1209 OrderedNumbering::LowerAlpha => 1,
1210 OrderedNumbering::UpperAlpha => 2,
1211 OrderedNumbering::LowerRoman => 3,
1212 OrderedNumbering::UpperRoman => 4,
1213 }
1214 }
1215}
1216
1217#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1219pub enum OrderedDelim {
1220 Period,
1221 ParenAfter,
1222 ParenBoth,
1223}
1224
1225impl OrderedDelim {
1226 fn to_c(self) -> c_int {
1227 match self {
1228 OrderedDelim::Period => 0,
1229 OrderedDelim::ParenAfter => 1,
1230 OrderedDelim::ParenBoth => 2,
1231 }
1232 }
1233}
1234
1235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1238pub enum Alignment {
1239 Default,
1240 Left,
1241 Right,
1242 Center,
1243}
1244
1245impl Alignment {
1246 fn to_c(self) -> c_int {
1247 match self {
1248 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
1249 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
1250 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
1251 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
1252 }
1253 }
1254
1255 fn from_c(v: c_int) -> Option<Self> {
1258 match v {
1259 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
1260 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
1261 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
1262 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
1263 _ => None,
1264 }
1265 }
1266}
1267
1268#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1270pub enum SmartPunctuation {
1271 LeftSingleQuote,
1272 RightSingleQuote,
1273 LeftDoubleQuote,
1274 RightDoubleQuote,
1275 Ellipses,
1276 EmDash,
1277 EnDash,
1278}
1279
1280impl SmartPunctuation {
1281 fn to_c(self) -> c_int {
1282 match self {
1283 SmartPunctuation::LeftSingleQuote => 0,
1284 SmartPunctuation::RightSingleQuote => 1,
1285 SmartPunctuation::LeftDoubleQuote => 2,
1286 SmartPunctuation::RightDoubleQuote => 3,
1287 SmartPunctuation::Ellipses => 4,
1288 SmartPunctuation::EmDash => 5,
1289 SmartPunctuation::EnDash => 6,
1290 }
1291 }
1292}
1293
1294#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1296pub enum DirectiveForm {
1297 Text,
1298 Leaf,
1299 Container,
1300}
1301
1302impl DirectiveForm {
1303 fn to_c(self) -> c_int {
1304 match self {
1305 DirectiveForm::Text => 0,
1306 DirectiveForm::Leaf => 1,
1307 DirectiveForm::Container => 2,
1308 }
1309 }
1310}
1311
1312fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
1316 match s {
1317 Some(x) => (x.as_ptr(), x.len(), 1),
1318 None => (std::ptr::null(), 0, 0),
1319 }
1320}
1321
1322#[derive(Debug)]
1329pub struct Builder {
1330 raw: NonNull<ffi::TwigBuilder>,
1331}
1332
1333impl Builder {
1334 pub fn new() -> Result<Self, Error> {
1336 let mut raw = std::ptr::null_mut();
1337 let status = unsafe { ffi::twig_builder_create(&mut raw) };
1338 Error::from_status(status)?;
1339 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1340 Ok(Self { raw })
1341 }
1342
1343 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
1346 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
1347 }
1348
1349 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
1351 self.emit(|b, out| unsafe { ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out) })
1352 }
1353
1354 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
1356 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
1357 }
1358
1359 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
1361 let (lp, ll, has) = opt_str(lang);
1362 self.emit(|b, out| unsafe { ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out) })
1363 }
1364
1365 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1367 self.emit(|b, out| unsafe {
1368 ffi::twig_builder_add_raw_block(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1369 })
1370 }
1371
1372 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
1374 self.emit(|b, out| unsafe {
1375 ffi::twig_builder_add_metadata(b, lang.as_ptr(), lang.len(), text.as_ptr(), text.len(), out)
1376 })
1377 }
1378
1379 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1381 self.emit(|b, out| unsafe {
1382 ffi::twig_builder_add_raw_inline(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1383 })
1384 }
1385
1386 pub fn add_smart_punctuation(&mut self, kind: SmartPunctuation, text: &str) -> Result<NodeId, Error> {
1388 self.emit(|b, out| unsafe {
1389 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
1390 })
1391 }
1392
1393 pub fn add_link(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1396 let (dp, dl, hd) = opt_str(destination);
1397 let (rp, rl, hr) = opt_str(reference);
1398 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
1399 }
1400
1401 pub fn add_image(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1403 let (dp, dl, hd) = opt_str(destination);
1404 let (rp, rl, hr) = opt_str(reference);
1405 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
1406 }
1407
1408 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
1410 self.emit(|b, out| unsafe { ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out) })
1411 }
1412
1413 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
1415 self.emit(|b, out| unsafe { ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out) })
1416 }
1417
1418 pub fn add_processing_instruction(&mut self, target: &str, data: &str) -> Result<NodeId, Error> {
1420 self.emit(|b, out| unsafe {
1421 ffi::twig_builder_add_processing_instruction(b, target.as_ptr(), target.len(), data.as_ptr(), data.len(), out)
1422 })
1423 }
1424
1425 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
1427 self.emit(|b, out| unsafe { ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out) })
1428 }
1429
1430 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
1432 self.emit(|b, out| unsafe {
1433 ffi::twig_builder_add_reference(b, label.as_ptr(), label.len(), destination.as_ptr(), destination.len(), out)
1434 })
1435 }
1436
1437 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
1439 self.emit(|b, out| unsafe { ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out) })
1440 }
1441
1442 pub fn add_ordered_list(
1444 &mut self,
1445 numbering: OrderedNumbering,
1446 delim: OrderedDelim,
1447 tight: bool,
1448 start: Option<u32>,
1449 ) -> Result<NodeId, Error> {
1450 let (start_val, has_start) = match start {
1451 Some(s) => (s, 1),
1452 None => (0, 0),
1453 };
1454 self.emit(|b, out| unsafe {
1455 ffi::twig_builder_add_ordered_list(b, numbering.to_c(), delim.to_c(), tight as c_int, start_val, has_start, out)
1456 })
1457 }
1458
1459 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
1461 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
1462 }
1463
1464 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
1466 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list_item(b, checked as c_int, out) })
1467 }
1468
1469 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
1471 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
1472 }
1473
1474 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
1476 self.emit(|b, out| unsafe { ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out) })
1477 }
1478
1479 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
1482 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
1483 let status = unsafe { ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len()) };
1484 Error::from_status(status)
1485 }
1486
1487 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
1491 let kvs: Vec<ffi::TwigKeyVal> = attrs
1492 .iter()
1493 .map(|(k, v)| ffi::TwigKeyVal {
1494 key: k.as_ptr(),
1495 key_len: k.len(),
1496 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
1497 value_len: v.map_or(0, |s| s.len()),
1498 })
1499 .collect();
1500 let status = unsafe { ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len()) };
1501 Error::from_status(status)
1502 }
1503
1504 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1507 let raw = self.raw.as_ptr();
1508 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
1509 }
1510
1511 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
1515 let raw = self.raw.as_ptr();
1516 let ffi_format: ffi::TwigFormat = format.into();
1517 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_serialize(raw, root.0, ffi_format as i32, ptr, len) })
1518 }
1519
1520 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1522 let raw = self.raw.as_ptr();
1523 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
1524 }
1525
1526 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1529 let raw = self.raw.as_ptr();
1530 collect_matches(|ptr, len| unsafe {
1531 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
1532 })
1533 }
1534
1535 fn emit(
1538 &mut self,
1539 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
1540 ) -> Result<NodeId, Error> {
1541 let mut id: u32 = 0;
1542 let status = call(self.raw.as_ptr(), &mut id);
1543 Error::from_status(status)?;
1544 Ok(NodeId(id))
1545 }
1546}
1547
1548impl Drop for Builder {
1549 fn drop(&mut self) {
1550 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
1551 }
1552}
1553
1554#[cfg(test)]
1555mod tests {
1556 use super::*;
1557
1558 #[test]
1559 fn abi_version_matches() {
1560 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
1564 }
1565
1566 #[test]
1567 fn parses_and_renders_markdown_html() {
1568 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1569 let html = doc.render_html().expect("render html");
1570 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
1571 }
1572
1573 #[test]
1574 fn parses_html_input() {
1575 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
1576 let html = doc.render_html().expect("render html");
1577 assert!(String::from_utf8_lossy(&html).contains("hi"));
1578 }
1579
1580 #[test]
1581 fn serialize_round_trips_and_cross_converts() {
1582 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1583
1584 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
1585 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
1586
1587 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
1589 }
1590
1591 #[test]
1592 fn serialize_markdown_to_djot() {
1593 let mut doc =
1594 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
1595 let djot = doc.serialize(Format::Djot).expect("serialize djot");
1596 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
1597 }
1598
1599 #[test]
1600 fn ast_json_dumps_the_tree() {
1601 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
1602 let json = doc.ast_json().expect("ast json");
1603 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1604 }
1605
1606 #[test]
1607 fn query_finds_nodes_by_selector() {
1608 let source = "# One\n\n## Two\n";
1609 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1610 let matches = doc.query("heading").expect("query");
1611
1612 assert_eq!(matches.len(), 2);
1613 for m in &matches {
1614 assert_eq!(m.kind, "heading");
1615 assert!(m.span.start < m.span.end);
1616 }
1617 }
1618
1619 #[test]
1620 fn query_recovers_code_spans() {
1621 let source = "prose `code` more prose\n";
1622 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1623 let matches = doc.query("verbatim").expect("query");
1624
1625 assert_eq!(matches.len(), 1);
1626 assert_eq!(&source[matches[0].span.clone()], "`code`");
1627 }
1628
1629 #[test]
1630 fn query_rejects_a_malformed_selector() {
1631 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
1632 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
1633 }
1634
1635 #[test]
1636 fn editor_edits_by_index_path() {
1637 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
1638 ed.replace_content("0.0", "bye").expect("replace_content");
1639 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
1640 }
1641
1642 #[test]
1643 fn flat_nodes_expose_element_name_and_attrs() {
1644 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
1648 let mut ed = Editor::new_ext(
1649 src.as_bytes(),
1650 Format::Markdown,
1651 MarkdownExtensions { html_elements: true, ..Default::default() },
1652 )
1653 .expect("editor");
1654 let nodes = ed.nodes().expect("nodes");
1655
1656 let source = nodes
1657 .iter()
1658 .find(|n| n.name.as_deref() == Some("source"))
1659 .expect("a <source> element node");
1660 assert_eq!(
1661 source.attrs,
1662 vec![
1663 ("media".to_string(), Some("(prefers-color-scheme: dark)".to_string())),
1664 ("srcset".to_string(), Some("d.svg".to_string())),
1665 ]
1666 );
1667
1668 let img = nodes.iter().find(|n| n.kind == "image").expect("an image node");
1671 assert!(img.name.is_none());
1672 assert_eq!(img.destination.as_deref(), Some("l.svg"));
1673
1674 let picture_kids_str = nodes.iter().find(|n| n.kind == "str");
1676 if let Some(s) = picture_kids_str {
1677 assert!(s.name.is_none() && s.attrs.is_empty());
1678 }
1679 }
1680
1681 #[test]
1682 fn editor_insert_child_and_delete() {
1683 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
1684 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1685 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
1686 ed.delete("0.1").expect("delete");
1687 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
1688 }
1689
1690 #[test]
1691 fn editor_edits_by_selector() {
1692 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1693 ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1694 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
1695 }
1696
1697 #[test]
1698 fn editor_locator_errors_are_distinct() {
1699 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
1700 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
1701 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
1702 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
1703 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
1705 }
1706
1707 #[test]
1708 fn editor_reparse_break_rolls_back() {
1709 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
1710 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
1711 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
1712 }
1713
1714 #[test]
1715 fn editor_leaf_content_is_not_editable() {
1716 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1717 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
1718 }
1719
1720 #[test]
1721 fn editor_query_reflects_current_tree() {
1722 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
1723 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1724 assert_eq!(ed.query("element").expect("query").len(), 3);
1726 let json = ed.ast_json().expect("ast_json");
1727 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1728 }
1729
1730 #[test]
1733 fn editor_edit_range_types_backspaces_and_reports_change() {
1734 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
1735
1736 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
1738 assert_eq!(ed.source_str().unwrap(), "aXb\n");
1739 assert_eq!(c.old, 1..1);
1740 assert_eq!(c.new, 1..2);
1741 assert_eq!(c.delta(), 1);
1742
1743 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
1745 assert_eq!(ed.source_str().unwrap(), "ab\n");
1746 assert_eq!(c2.old, 1..2);
1747 assert_eq!(c2.new, 1..1);
1748 assert_eq!(c2.delta(), -1);
1749 }
1750
1751 #[test]
1752 fn editor_edit_range_rejects_bad_ranges() {
1753 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1754 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"); }
1758
1759 #[test]
1760 fn editor_last_change_reports_locator_ops_too() {
1761 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1762 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1765 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
1766 let c = ed.last_change().expect("a change was recorded");
1767 assert_eq!(c.old, 7..13);
1769 assert_eq!(c.new, 7..17);
1770 }
1771
1772 #[test]
1773 fn editor_nodes_is_a_walkable_flat_tree() {
1774 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1775 let nodes = ed.nodes().expect("nodes");
1776 assert!(!nodes.is_empty());
1777
1778 for (i, n) in nodes.iter().enumerate() {
1780 assert_eq!(n.id, NodeId(i as u32));
1781 }
1782 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
1784 assert_eq!(roots.len(), 1);
1785 assert_eq!(roots[0].kind, "doc");
1786
1787 let heading = nodes.iter().find(|n| n.kind == "heading").expect("a heading");
1789 assert_eq!(heading.level, Some(1));
1790 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
1791
1792 assert_eq!(heading.head, None);
1794 assert_eq!(heading.alignment, None);
1795
1796 for n in nodes.iter().filter(|n| n.parent.is_some()) {
1799 let p = &nodes[n.parent.unwrap().0 as usize];
1800 let mut kid = p.first_child;
1801 let mut seen = false;
1802 while let Some(NodeId(k)) = kid {
1803 if k == n.id.0 {
1804 seen = true;
1805 break;
1806 }
1807 kid = nodes[k as usize].next_sibling;
1808 }
1809 assert!(seen, "node {:?} not found among its parent's children", n.id);
1810 }
1811 }
1812
1813 #[test]
1814 fn editor_child_spans_and_subtree_agree_with_nodes() {
1815 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
1816 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
1817 let all = ed.nodes().expect("nodes");
1818 let doc = all.iter().find(|n| n.kind == "doc").expect("doc");
1819
1820 let top = ed.child_spans(None).expect("child_spans");
1823 let mut want = Vec::new();
1824 let mut c = doc.first_child;
1825 while let Some(id) = c {
1826 want.push(id);
1827 c = all[id.0 as usize].next_sibling;
1828 }
1829 assert_eq!(top.len(), want.len(), "top-level count");
1830 for (m, id) in top.iter().zip(&want) {
1831 assert_eq!(m.node_id, id.0, "child id");
1832 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
1833 assert_eq!(m.span, all[id.0 as usize].span, "child span");
1834 }
1835 assert!(src[top[0].span.clone()].starts_with('#'), "first block is the heading");
1837
1838 let list = top.iter().find(|m| m.kind.ends_with("list")).expect("a list");
1840 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
1841 assert_eq!(items.len(), 2);
1842 assert!(items.iter().all(|m| m.kind == "list_item"), "items: {items:?}");
1843
1844 let para = top.iter().find(|m| m.kind == "para").expect("a para").node_id;
1846 let sub = ed.subtree(NodeId(para)).expect("subtree");
1847 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
1848 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
1849 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
1850 assert_eq!(sub[0].kind, "para");
1851 for (i, n) in sub.iter().enumerate() {
1852 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
1853 for link in [n.parent, n.first_child, n.next_sibling].into_iter().flatten() {
1854 assert!((link.0 as usize) < sub.len(), "link {link:?} escapes the subtree");
1855 }
1856 }
1857 assert!(
1858 src[sub[0].span.clone()].starts_with("Hello"),
1859 "absolute span: {:?}",
1860 &src[sub[0].span.clone()]
1861 );
1862
1863 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<String> {
1865 let mut out = Vec::new();
1866 let mut stack = vec![root];
1867 while let Some(id) = stack.pop() {
1868 let n = &all[id.0 as usize];
1869 out.push(n.kind.clone());
1870 let mut c = n.first_child;
1871 while let Some(cid) = c {
1872 stack.push(cid);
1873 c = all[cid.0 as usize].next_sibling;
1874 }
1875 }
1876 out
1877 }
1878 let mut want_kinds = arena_kinds(&all, NodeId(para));
1879 let mut got_kinds: Vec<String> = sub.iter().map(|n| n.kind.clone()).collect();
1880 want_kinds.sort();
1881 got_kinds.sort();
1882 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
1883
1884 assert!(matches!(ed.subtree(NodeId(9999)), Err(Error::InvalidArgument)));
1886 }
1887
1888 #[test]
1889 fn flat_nodes_carry_table_head_and_alignment() {
1890 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
1894 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
1895 let nodes = ed.nodes().expect("nodes");
1896
1897 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == "row").collect();
1898 assert_eq!(rows.len(), 2, "a header row and one body row");
1899 assert_eq!(rows[0].head, Some(true), "first row is the header");
1900 assert_eq!(rows[1].head, Some(false), "second row is a body row");
1901
1902 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == "cell").collect();
1903 assert_eq!(cells.len(), 4);
1904 assert_eq!(cells[0].alignment, Some(Alignment::Left));
1906 assert_eq!(cells[1].alignment, Some(Alignment::Right));
1907 assert_eq!(cells[2].alignment, Some(Alignment::Left));
1908 assert_eq!(cells[3].alignment, Some(Alignment::Right));
1909 assert_eq!(cells[0].head, Some(true));
1911 assert_eq!(cells[2].head, Some(false));
1912
1913 let mut plain = Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
1916 let pnodes = plain.nodes().expect("nodes");
1917 let pcell = pnodes.iter().find(|n| n.kind == "cell").expect("a cell");
1918 assert_eq!(pcell.alignment, Some(Alignment::Default));
1919 }
1920
1921 #[test]
1922 fn editor_node_at_and_ancestors_hit_test_offsets() {
1923 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1924
1925 let m = ed.node_at(2).expect("node_at").expect("a node covers offset 2");
1927 assert!(m.span.contains(&2));
1928
1929 let chain = ed.ancestors_at(2).expect("ancestors_at");
1931 assert!(!chain.is_empty());
1932 assert_eq!(chain[0].kind, "doc");
1933 assert_eq!(chain.last().unwrap().node_id, m.node_id);
1934
1935 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
1937 }
1938
1939 #[test]
1942 fn editor_wrap_and_toggle_inline_round_trip() {
1943 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
1944
1945 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
1947 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
1948 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
1949
1950 ed.toggle_inline(4, 8, InlineKind::Strong).expect("toggle off");
1952 assert_eq!(ed.source_str().unwrap(), "a word b\n");
1953
1954 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
1956 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
1957 }
1958
1959 #[test]
1960 fn editor_inline_kind_support_is_format_specific() {
1961 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
1963 assert_eq!(md.wrap_range(2, 6, InlineKind::Mark), Err(Error::UnsupportedFormat));
1964
1965 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
1967 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
1968 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
1969 }
1970
1971 #[test]
1972 fn editor_toggle_strips_verbatim_via_content_span() {
1973 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
1974 ed.toggle_inline(2, 8, InlineKind::Verbatim).expect("toggle code off");
1976 assert_eq!(ed.source_str().unwrap(), "a code b\n");
1977
1978 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
1981 ed2.toggle_inline(2, 7, InlineKind::Verbatim).expect("toggle multi off");
1982 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
1983 }
1984
1985 #[test]
1986 fn editor_set_block_switches_para_and_heading_levels() {
1987 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
1988
1989 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
1991 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
1992
1993 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
1995 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
1996
1997 ed.set_block(2, BlockKind::Paragraph).expect("to para");
1999 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
2000 }
2001
2002 #[test]
2003 fn editor_set_block_rejects_bad_level_and_format() {
2004 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2005 assert_eq!(md.set_block(0, BlockKind::Heading(9)), Err(Error::InvalidArgument));
2006
2007 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2008 assert_eq!(xml.set_block(1, BlockKind::Heading(1)), Err(Error::UnsupportedFormat));
2009 }
2010
2011 #[test]
2012 fn editor_toggle_block_container_round_trips() {
2013 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
2014
2015 let c = ed
2016 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
2017 .expect("quote on");
2018 assert_eq!(ed.source_str().unwrap(), "> a\n");
2019 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
2020
2021 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2022 .expect("quote off");
2023 assert_eq!(ed.source_str().unwrap(), "a\n");
2024 }
2025
2026 #[test]
2027 fn editor_toggle_block_container_nests_a_partial_selection() {
2028 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
2029
2030 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2033 .expect("nest");
2034 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
2035
2036 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
2038 .expect("peel");
2039 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
2040 }
2041
2042 #[test]
2043 fn editor_toggle_block_container_numbers_and_converts_lists() {
2044 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
2045
2046 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
2048 .expect("ordered on");
2049 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
2050
2051 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
2053 .expect("convert");
2054 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
2055 }
2056
2057 #[test]
2058 fn editor_toggle_block_container_rejects_unspellable_format() {
2059 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2060 assert_eq!(
2061 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
2062 Err(Error::UnsupportedFormat)
2063 );
2064 }
2065
2066 #[test]
2067 fn editor_insert_link_wraps_and_repoints() {
2068 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
2069
2070 ed.insert_link(2, 6, "http://x.dev").expect("link");
2071 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
2072
2073 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
2075 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
2076 }
2077
2078 #[test]
2079 fn editor_insert_link_repoints_an_autolink() {
2080 for format in [Format::Markdown, Format::Djot] {
2085 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
2086 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
2087 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
2088
2089 let nodes = ed.nodes().expect("nodes");
2091 let url = nodes
2092 .iter()
2093 .find(|n| n.kind == "url")
2094 .expect("still an autolink");
2095 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
2096 assert!(!nodes.iter().any(|n| n.kind == "link"));
2097 }
2098 }
2099
2100 #[test]
2101 fn editor_insert_link_escapes_the_destination() {
2102 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
2105 dj.insert_link(0, 1, "a)b").expect("link");
2106 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
2107
2108 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
2112 md.insert_link(0, 1, "a b").expect("link");
2113 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
2114
2115 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
2116 dj2.insert_link(0, 1, "a b").expect("link");
2117 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
2118 }
2119
2120 #[test]
2121 fn editor_insert_link_rejects_a_newline_destination() {
2122 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
2123 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
2124
2125 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2126 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
2127 }
2128
2129 #[test]
2130 fn editor_undo_redo_round_trip() {
2131 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2132 ed.edit_range(5, 5, "!").expect("edit");
2133 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2134
2135 let change = ed.undo().expect("undo ok").expect("something to undo");
2136 assert_eq!(ed.source_str().unwrap(), "hello\n");
2137 assert_eq!(change.new.end, 5);
2138 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
2139
2140 ed.redo().expect("redo ok").expect("something to redo");
2141 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2142 }
2143
2144 #[test]
2145 fn editor_coalesce_folds_a_run() {
2146 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2147 ed.edit_range(0, 0, "a").expect("edit");
2148 ed.edit_range(1, 1, "b").expect("edit");
2149 ed.coalesce_last_undo().expect("coalesce");
2150 assert_eq!(ed.source_str().unwrap(), "ab\n");
2151 ed.undo().expect("undo ok").expect("something to undo");
2153 assert_eq!(ed.source_str().unwrap(), "\n");
2154 assert!(ed.undo().expect("undo ok").is_none());
2155 }
2156
2157 #[test]
2158 fn editor_revision_bumps_per_successful_mutation() {
2159 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
2160 assert_eq!(ed.revision(), 0);
2161 ed.edit_range(1, 1, "y").expect("edit");
2162 assert_eq!(ed.revision(), 1);
2163
2164 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
2166 assert_eq!(xml.revision(), 0);
2167 assert!(xml.replace_content("0", "<b>").is_err());
2168 assert_eq!(xml.revision(), 0);
2169
2170 ed.undo().expect("undo ok").expect("something to undo");
2172 assert_eq!(ed.revision(), 2);
2173 ed.redo().expect("redo ok").expect("something to redo");
2174 assert_eq!(ed.revision(), 3);
2175 }
2176
2177 #[test]
2178 fn editor_dirty_range_tracks_and_clears() {
2179 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
2180 assert_eq!(ed.dirty_range(), None);
2182
2183 ed.edit_range(2, 2, "XY").expect("edit");
2185 assert_eq!(ed.dirty_range(), Some(2..4));
2186
2187 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
2191 assert!(d.start <= 2 && d.end >= 10, "range {d:?} must cover both edits");
2192
2193 let rev = ed.revision();
2195 ed.clear_dirty();
2196 assert_eq!(ed.dirty_range(), None);
2197 assert_eq!(ed.revision(), rev);
2198
2199 ed.undo().expect("undo ok").expect("something to undo");
2201 assert!(ed.dirty_range().is_some());
2202 }
2203
2204 #[test]
2205 fn editor_caret_blob_follows_undo_and_redo() {
2206 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2207 assert!(ed.caret_blob().unwrap().is_empty());
2208
2209 ed.set_caret_blob(b"before").expect("set caret");
2211 ed.edit_range(5, 5, "!").expect("edit");
2212 assert!(ed.caret_blob().unwrap().is_empty());
2214 ed.set_caret_blob(b"after").expect("set caret");
2215
2216 ed.undo().expect("undo ok").expect("something to undo");
2218 assert_eq!(ed.source_str().unwrap(), "hello\n");
2219 assert_eq!(ed.caret_blob().unwrap(), b"before");
2220
2221 ed.redo().expect("redo ok").expect("something to redo");
2223 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2224 assert_eq!(ed.caret_blob().unwrap(), b"after");
2225 }
2226
2227 #[test]
2228 fn editor_coalesced_run_keeps_the_pre_run_caret() {
2229 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2230 ed.set_caret_blob(b"c0").expect("set caret");
2231 ed.edit_range(0, 0, "a").expect("edit");
2232 ed.set_caret_blob(b"c1").expect("set caret");
2233 ed.edit_range(1, 1, "b").expect("edit");
2234 ed.coalesce_last_undo().expect("coalesce");
2235 ed.set_caret_blob(b"c2").expect("set caret");
2236
2237 ed.undo().expect("undo ok").expect("something to undo");
2239 assert_eq!(ed.source_str().unwrap(), "\n");
2240 assert_eq!(ed.caret_blob().unwrap(), b"c0");
2241 }
2242
2243 #[test]
2244 fn editor_set_block_converts_setext_heading() {
2245 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
2247 ed.set_block(0, BlockKind::Heading(1)).expect("setext to atx");
2248 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
2249 }
2250
2251 #[test]
2252 fn editor_unwrap_and_smart_delete() {
2253 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
2254 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
2256
2257 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
2258 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
2260 }
2261
2262 #[test]
2263 fn editor_directives_require_the_extension_flag() {
2264 let src = ":::vis{.public}\nhi\n:::\n";
2265 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
2268 assert_eq!(plain.query("directive").expect("query").len(), 0);
2269 let mut ext = Editor::new_ext(
2271 src.as_bytes(),
2272 Format::Markdown,
2273 MarkdownExtensions { directives: true, ..Default::default() },
2274 )
2275 .expect("editor");
2276 assert_eq!(ext.query("directive").expect("query").len(), 1);
2277 }
2278
2279 #[test]
2280 fn document_html_elements_make_embedded_img_queryable() {
2281 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
2282 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
2284 assert_eq!(plain.query("image").expect("query").len(), 0);
2285 let mut ext = Document::parse_str_with(
2287 src,
2288 Format::Markdown,
2289 MarkdownExtensions { html_elements: true, ..Default::default() },
2290 )
2291 .expect("parse");
2292 let images = ext.query("image").expect("query");
2293 assert_eq!(images.len(), 1);
2294 assert_eq!(images[0].kind, "image");
2295 }
2296
2297 #[test]
2298 fn editor_filter_public_audience_view() {
2299 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
2300 let mut ed = Editor::new_ext(
2301 src.as_bytes(),
2302 Format::Markdown,
2303 MarkdownExtensions { directives: true, ..Default::default() },
2304 )
2305 .expect("editor");
2306 ed.filter(
2308 "directive[name=vis]",
2309 Some("directive[class~=public]"),
2310 true,
2311 )
2312 .expect("filter");
2313 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
2314 }
2315
2316 #[test]
2317 fn editor_filter_rejects_a_malformed_selector() {
2318 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2319 assert_eq!(ed.filter("list >", None, false), Err(Error::InvalidArgument));
2320 }
2321
2322 #[test]
2323 fn builder_builds_and_renders_a_document() {
2324 let mut b = Builder::new().expect("builder");
2325
2326 let title = b.add_text(TextKind::Str, "Title").unwrap();
2328 let heading = b.add_heading(1).unwrap();
2329 b.set_children(heading, &[title]).unwrap();
2330
2331 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
2332 let world = b.add_text(TextKind::Str, "world").unwrap();
2333 let emph = b.add(VoidKind::Emph).unwrap();
2334 b.set_children(emph, &[world]).unwrap();
2335 let para = b.add(VoidKind::Para).unwrap();
2336 b.set_children(para, &[hello, emph]).unwrap();
2337
2338 let doc = b.add(VoidKind::Doc).unwrap();
2339 b.set_children(doc, &[heading, para]).unwrap();
2340
2341 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
2342 assert!(html.contains("<h1>Title</h1>"), "{html}");
2343 assert!(html.contains("<em>world</em>"), "{html}");
2344
2345 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
2346 assert!(md.contains("# Title"), "{md}");
2347 assert!(md.contains("*world*"), "{md}");
2348
2349 let matches = b.query(doc, "heading").unwrap();
2350 assert_eq!(matches.len(), 1);
2351 assert_eq!(matches[0].kind, "heading");
2352
2353 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
2354 assert!(json.contains("\"kind\": \"doc\""), "{json}");
2355 }
2356
2357 #[test]
2358 fn builder_element_with_attributes() {
2359 let mut b = Builder::new().expect("builder");
2360 let inner = b.add_text(TextKind::Str, "hi").unwrap();
2361 let el = b.add_element("section").unwrap();
2362 b.set_children(el, &[inner]).unwrap();
2363 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)]).unwrap();
2364
2365 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
2366 assert!(html.contains("<section"), "{html}");
2367 assert!(html.contains("class=\"note\""), "{html}");
2368 assert!(html.contains("hidden"), "{html}");
2369 }
2370
2371 #[test]
2372 fn builder_lists_round_trip_to_markdown() {
2373 let mut b = Builder::new().expect("builder");
2374
2375 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
2377 let one_para = b.add(VoidKind::Para).unwrap();
2378 b.set_children(one_para, &[one_txt]).unwrap();
2379 let one = b.add(VoidKind::ListItem).unwrap();
2380 b.set_children(one, &[one_para]).unwrap();
2381
2382 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
2383 let two_para = b.add(VoidKind::Para).unwrap();
2384 b.set_children(two_para, &[two_txt]).unwrap();
2385 let two = b.add(VoidKind::ListItem).unwrap();
2386 b.set_children(two, &[two_para]).unwrap();
2387
2388 let list = b
2389 .add_ordered_list(OrderedNumbering::Decimal, OrderedDelim::Period, true, Some(1))
2390 .unwrap();
2391 b.set_children(list, &[one, two]).unwrap();
2392 let doc = b.add(VoidKind::Doc).unwrap();
2393 b.set_children(doc, &[list]).unwrap();
2394
2395 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
2396 assert!(md.contains("1. one"), "{md}");
2397 assert!(md.contains("2. two"), "{md}");
2398 }
2399
2400 #[test]
2401 fn builder_rejects_invalid_kind_and_id() {
2402 let b = Builder::new().expect("builder");
2403 let mut id = 0u32;
2407 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
2408 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2409
2410 let mut ptr = std::ptr::null();
2412 let mut len = 0usize;
2413 let status = unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
2414 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2415 }
2416}