1mod error;
2
3pub(crate) use twig_sys as ffi;
8
9use std::ops::Range;
10use std::os::raw::{c_char, c_int};
11use std::ptr::NonNull;
12
13pub use error::Error;
14pub use ffi::TwigSpan as Span;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum Format {
18 Djot,
19 Markdown,
20 Xml,
21 Html,
22}
23
24impl From<Format> for ffi::TwigFormat {
25 fn from(value: Format) -> Self {
26 match value {
27 Format::Djot => ffi::TwigFormat::Djot,
28 Format::Markdown => ffi::TwigFormat::Markdown,
29 Format::Xml => ffi::TwigFormat::Xml,
30 Format::Html => ffi::TwigFormat::Html,
31 }
32 }
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct QueryMatch {
38 pub node_id: u32,
40 pub span: Range<usize>,
42 pub content_span: Option<Range<usize>>,
45 pub kind: String,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct Change {
57 pub old: Range<usize>,
58 pub new: Range<usize>,
59}
60
61impl Change {
62 pub fn delta(&self) -> isize {
64 self.new.len() as isize - self.old.len() as isize
65 }
66
67 fn from_ffi(c: ffi::TwigChange) -> Self {
68 Change {
69 old: c.old_span.start..c.old_span.end,
70 new: c.new_span.start..c.new_span.end,
71 }
72 }
73}
74
75#[derive(Clone, Debug, Eq, PartialEq)]
86#[non_exhaustive]
87pub struct FlatNode {
88 pub id: NodeId,
89 pub parent: Option<NodeId>,
90 pub first_child: Option<NodeId>,
91 pub next_sibling: Option<NodeId>,
92 pub span: Range<usize>,
93 pub content_span: Option<Range<usize>>,
94 pub level: Option<u32>,
96 pub kind: String,
97 pub text: Option<String>,
98 pub destination: Option<String>,
99 pub head: Option<bool>,
102 pub alignment: Option<Alignment>,
108 pub name: Option<String>,
113 pub attrs: Vec<(String, Option<String>)>,
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub enum InlineKind {
125 Strong,
126 Emph,
127 Verbatim,
128 Mark,
129 Superscript,
130 Subscript,
131 Insert,
132 Delete,
133}
134
135impl InlineKind {
136 fn to_c(self) -> c_int {
137 match self {
138 InlineKind::Strong => 0,
139 InlineKind::Emph => 1,
140 InlineKind::Verbatim => 2,
141 InlineKind::Mark => 3,
142 InlineKind::Superscript => 4,
143 InlineKind::Subscript => 5,
144 InlineKind::Insert => 6,
145 InlineKind::Delete => 7,
146 }
147 }
148}
149
150#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152pub enum BlockKind {
153 Paragraph,
154 Heading(u32),
156}
157
158impl BlockKind {
159 fn to_c(self) -> (c_int, u32) {
161 match self {
162 BlockKind::Paragraph => (0, 0),
163 BlockKind::Heading(level) => (1, level),
164 }
165 }
166}
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub enum BlockContainerKind {
175 BlockQuote,
176 BulletList,
177 OrderedList,
178}
179
180impl BlockContainerKind {
181 fn to_c(self) -> c_int {
182 match self {
183 BlockContainerKind::BlockQuote => 0,
184 BlockContainerKind::BulletList => 1,
185 BlockContainerKind::OrderedList => 2,
186 }
187 }
188}
189
190#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub struct Version {
192 pub major: u8,
193 pub minor: u8,
194 pub patch: u8,
195}
196
197pub fn version() -> Version {
198 let packed = unsafe { ffi::twig_version() };
199 Version {
200 major: (packed >> 16) as u8,
201 minor: (packed >> 8) as u8,
202 patch: packed as u8,
203 }
204}
205
206pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
212
213pub fn abi_version() -> u32 {
219 unsafe { ffi::twig_abi_version() }
220}
221
222pub fn version_string() -> &'static str {
223 let ptr = unsafe { ffi::twig_version_string() };
224 unsafe { std::ffi::CStr::from_ptr(ptr) }
225 .to_str()
226 .unwrap_or("")
227}
228
229#[derive(Debug)]
230pub struct Document {
231 raw: NonNull<ffi::TwigDocument>,
232}
233
234impl Document {
235 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
236 Self::parse_with(input, format, MarkdownExtensions::default())
237 }
238
239 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
240 Self::parse(input.as_bytes(), format)
241 }
242
243 pub fn parse_with(
249 input: &[u8],
250 format: Format,
251 extensions: MarkdownExtensions,
252 ) -> Result<Self, Error> {
253 let mut raw = std::ptr::null_mut();
254 let ffi_format: ffi::TwigFormat = format.into();
255 let status = unsafe {
256 ffi::twig_parse_ext(
257 input.as_ptr(),
258 input.len(),
259 ffi_format as i32,
260 extensions.to_flags(),
261 &mut raw,
262 )
263 };
264 Error::from_status(status)?;
265 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
266 Ok(Self { raw })
267 }
268
269 pub fn parse_str_with(
271 input: &str,
272 format: Format,
273 extensions: MarkdownExtensions,
274 ) -> Result<Self, Error> {
275 Self::parse_with(input.as_bytes(), format, extensions)
276 }
277
278 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
281 let raw = self.raw.as_ptr();
282 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
283 }
284
285 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
291 let raw = self.raw.as_ptr();
292 let ffi_format: ffi::TwigFormat = format.into();
293 collect_bytes(|ptr, len| unsafe {
294 ffi::twig_document_serialize(raw, ffi_format as i32, ptr, len)
295 })
296 }
297
298 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
301 let raw = self.raw.as_ptr();
302 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
303 }
304
305 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
314 let raw = self.raw.as_ptr();
315 collect_matches(|ptr, len| unsafe {
316 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
317 })
318 }
319}
320
321impl Drop for Document {
322 fn drop(&mut self) {
323 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
324 }
325}
326
327#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
333pub struct MarkdownExtensions {
334 pub directives: bool,
336 pub math: bool,
338 pub html_elements: bool,
343}
344
345impl MarkdownExtensions {
346 fn to_flags(self) -> u32 {
347 let mut flags = 0;
348 if self.directives {
349 flags |= ffi::TWIG_MD_DIRECTIVES;
350 }
351 if self.math {
352 flags |= ffi::TWIG_MD_MATH;
353 }
354 if self.html_elements {
355 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
356 }
357 flags
358 }
359}
360
361#[derive(Debug)]
367pub struct Editor {
368 raw: NonNull<ffi::TwigEditor>,
369}
370
371impl Editor {
372 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
375 let mut raw = std::ptr::null_mut();
376 let ffi_format: ffi::TwigFormat = format.into();
377 let status =
378 unsafe { ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw) };
379 Error::from_status(status)?;
380 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
381 Ok(Self { raw })
382 }
383
384 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
385 Self::new(input.as_bytes(), format)
386 }
387
388 pub fn new_ext(input: &[u8], format: Format, extensions: MarkdownExtensions) -> Result<Self, Error> {
393 let mut raw = std::ptr::null_mut();
394 let ffi_format: ffi::TwigFormat = format.into();
395 let status = unsafe {
396 ffi::twig_editor_create_ext(
397 input.as_ptr(),
398 input.len(),
399 ffi_format as i32,
400 extensions.to_flags(),
401 &mut raw,
402 )
403 };
404 Error::from_status(status)?;
405 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
406 Ok(Self { raw })
407 }
408
409 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
411 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
412 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
413 })
414 }
415
416 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
419 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
420 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
421 })
422 }
423
424 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
426 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
427 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
428 })
429 }
430
431 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
433 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
434 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
435 })
436 }
437
438 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
441 let status = unsafe {
442 ffi::twig_editor_insert_child(
443 self.raw.as_ptr(),
444 locator.as_ptr(),
445 locator.len(),
446 index,
447 text.as_ptr(),
448 text.len(),
449 )
450 };
451 Error::from_status(status)
452 }
453
454 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
457 let status = unsafe {
458 ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len())
459 };
460 Error::from_status(status)
461 }
462
463 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
466 let status = unsafe {
467 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
468 };
469 Error::from_status(status)
470 }
471
472 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
476 let status = unsafe {
477 ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len())
478 };
479 Error::from_status(status)
480 }
481
482 pub fn filter(&mut self, drop: &str, keep: Option<&str>, unwrap_kept: bool) -> Result<(), Error> {
487 let (keep_ptr, keep_len) = match keep {
488 Some(k) => (k.as_ptr(), k.len()),
489 None => (std::ptr::null(), 0),
490 };
491 let status = unsafe {
492 ffi::twig_editor_filter(
493 self.raw.as_ptr(),
494 drop.as_ptr(),
495 drop.len(),
496 keep_ptr,
497 keep_len,
498 unwrap_kept as i32,
499 )
500 };
501 Error::from_status(status)
502 }
503
504 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
506 let raw = self.raw.as_ptr();
507 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
508 }
509
510 pub fn source_str(&mut self) -> Result<String, Error> {
512 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
513 }
514
515 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
518 let raw = self.raw.as_ptr();
519 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
520 }
521
522 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
525 let raw = self.raw.as_ptr();
526 collect_matches(|ptr, len| unsafe {
527 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
528 })
529 }
530
531 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
541 let mut change = ffi::TwigChange {
542 old_span: ffi::TwigSpan { start: 0, end: 0 },
543 new_span: ffi::TwigSpan { start: 0, end: 0 },
544 };
545 let status = unsafe {
546 ffi::twig_editor_edit_range(
547 self.raw.as_ptr(),
548 start,
549 end,
550 text.as_ptr(),
551 text.len(),
552 &mut change,
553 )
554 };
555 Error::from_status(status)?;
556 Ok(Change::from_ffi(change))
557 }
558
559 pub fn last_change(&mut self) -> Option<Change> {
565 let mut change = ffi::TwigChange {
566 old_span: ffi::TwigSpan { start: 0, end: 0 },
567 new_span: ffi::TwigSpan { start: 0, end: 0 },
568 };
569 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
570 match status.0 {
571 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
572 _ => None,
573 }
574 }
575
576 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
581 let mut change = ffi::TwigChange {
582 old_span: ffi::TwigSpan { start: 0, end: 0 },
583 new_span: ffi::TwigSpan { start: 0, end: 0 },
584 };
585 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
586 if status.0 == ffi::TwigStatus::NOT_FOUND {
587 return Ok(None);
588 }
589 Error::from_status(status)?;
590 Ok(Some(Change::from_ffi(change)))
591 }
592
593 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
597 let mut change = ffi::TwigChange {
598 old_span: ffi::TwigSpan { start: 0, end: 0 },
599 new_span: ffi::TwigSpan { start: 0, end: 0 },
600 };
601 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
602 if status.0 == ffi::TwigStatus::NOT_FOUND {
603 return Ok(None);
604 }
605 Error::from_status(status)?;
606 Ok(Some(Change::from_ffi(change)))
607 }
608
609 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
614 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
615 Error::from_status(status)
616 }
617
618 pub fn revision(&mut self) -> u64 {
624 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
625 }
626
627 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
648 let mut span = ffi::TwigSpan { start: 0, end: 0 };
649 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
650 match status.0 {
651 ffi::TwigStatus::OK => Some(span.start..span.end),
652 _ => None,
653 }
654 }
655
656 pub fn clear_dirty(&mut self) {
661 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
662 }
663
664 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
672 let status =
673 unsafe { ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len()) };
674 Error::from_status(status)
675 }
676
677 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
682 let raw = self.raw.as_ptr();
683 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
684 }
685
686 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
691 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
692 let mut len = 0usize;
693 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
694 Error::from_status(status)?;
695 if len == 0 {
696 return Ok(Vec::new());
697 }
698 if ptr.is_null() {
699 return Err(Error::Internal);
700 }
701 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
702 raw.iter().map(flat_node_from_ffi).collect()
703 }
704
705 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
712 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
713 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
714 let mut len = 0usize;
715 let status =
716 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
717 Error::from_status(status)?;
718 if len == 0 || ptr.is_null() {
719 return Ok(Vec::new());
720 }
721 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
722 raw.iter().map(query_match_from_ffi).collect()
723 }
724
725 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
733 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
734 let mut len = 0usize;
735 let status =
736 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
737 Error::from_status(status)?;
738 if len == 0 || ptr.is_null() {
739 return Ok(Vec::new());
740 }
741 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
742 raw.iter().map(flat_node_from_ffi).collect()
743 }
744
745 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
750 let mut m = ffi::TwigQueryMatch {
751 node_id: 0,
752 span: ffi::TwigSpan { start: 0, end: 0 },
753 content_span: ffi::TwigSpan { start: 0, end: 0 },
754 has_content_span: 0,
755 kind: std::ptr::null(),
756 };
757 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
758 match status.0 {
759 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
760 ffi::TwigStatus::NOT_FOUND => Ok(None),
761 _ => Err(Error::from_status(status).unwrap_err()),
762 }
763 }
764
765 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
769 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
770 let mut len = 0usize;
771 let status =
772 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
773 match status.0 {
774 ffi::TwigStatus::OK => {}
775 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
776 _ => return Err(Error::from_status(status).unwrap_err()),
777 }
778 if len == 0 || ptr.is_null() {
779 return Ok(Vec::new());
780 }
781 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
782 raw.iter().map(query_match_from_ffi).collect()
783 }
784
785 pub fn wrap_range(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
793 self.change_op(|ed, out| unsafe {
794 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
795 })
796 }
797
798 pub fn toggle_inline(&mut self, start: usize, end: usize, kind: InlineKind) -> Result<Change, Error> {
803 self.change_op(|ed, out| unsafe {
804 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
805 })
806 }
807
808 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
815 let (block_kind, level) = kind.to_c();
816 self.change_op(|ed, out| unsafe {
817 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
818 })
819 }
820
821 pub fn toggle_block_container(
844 &mut self,
845 start: usize,
846 end: usize,
847 kind: BlockContainerKind,
848 ) -> Result<Change, Error> {
849 self.change_op(|ed, out| unsafe {
850 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
851 })
852 }
853
854 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
866 self.change_op(|ed, out| unsafe {
867 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
868 })?;
869 Ok(())
870 }
871
872 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
881 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
882 }
883
884 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
887 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
888 }
889
890 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
892 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
893 }
894
895 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
897 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
898 }
899
900 pub fn table_set_alignment(&mut self, offset: usize, alignment: Alignment) -> Result<(), Error> {
902 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
903 }
904
905 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
907 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
908 }
909
910 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
912 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
913 }
914
915 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
916 self.change_op(|ed, out| unsafe {
917 ffi::twig_editor_table_edit(ed, offset, op, arg, out)
918 })?;
919 Ok(())
920 }
921
922 pub fn insert_link(
967 &mut self,
968 start: usize,
969 end: usize,
970 destination: &str,
971 ) -> Result<Change, Error> {
972 self.change_op(|ed, out| unsafe {
973 ffi::twig_editor_insert_link(
974 ed,
975 start,
976 end,
977 destination.as_ptr(),
978 destination.len(),
979 out,
980 )
981 })
982 }
983
984 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
1005 self.change_op(|ed, out| unsafe {
1006 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
1007 })
1008 }
1009
1010 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
1024 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
1025 }
1026
1027 fn change_op(
1030 &mut self,
1031 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
1032 ) -> Result<Change, Error> {
1033 let mut change = ffi::TwigChange {
1034 old_span: ffi::TwigSpan { start: 0, end: 0 },
1035 new_span: ffi::TwigSpan { start: 0, end: 0 },
1036 };
1037 let status = op(self.raw.as_ptr(), &mut change);
1038 Error::from_status(status)?;
1039 Ok(Change::from_ffi(change))
1040 }
1041
1042 fn apply(
1044 &mut self,
1045 locator: &str,
1046 text: &str,
1047 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
1048 ) -> Result<(), Error> {
1049 let status = op(
1050 self.raw.as_ptr(),
1051 locator.as_ptr(),
1052 locator.len(),
1053 text.as_ptr(),
1054 text.len(),
1055 );
1056 Error::from_status(status)
1057 }
1058}
1059
1060impl Drop for Editor {
1061 fn drop(&mut self) {
1062 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
1063 }
1064}
1065
1066fn collect_bytes(
1071 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
1072) -> Result<Vec<u8>, Error> {
1073 let mut ptr = std::ptr::null();
1074 let mut len = 0usize;
1075 let status = call(&mut ptr, &mut len);
1076 Error::from_status(status)?;
1077 if len == 0 {
1078 return Ok(Vec::new());
1079 }
1080 if ptr.is_null() {
1081 return Err(Error::Internal);
1082 }
1083 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1084 Ok(bytes.to_vec())
1085}
1086
1087fn collect_matches(
1090 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
1091) -> Result<Vec<QueryMatch>, Error> {
1092 let mut ptr = std::ptr::null();
1093 let mut len = 0usize;
1094 let status = call(&mut ptr, &mut len);
1095 Error::from_status(status)?;
1096 if len == 0 {
1097 return Ok(Vec::new());
1098 }
1099 if ptr.is_null() {
1100 return Err(Error::Internal);
1101 }
1102 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1103 matches.iter().map(query_match_from_ffi).collect()
1104}
1105
1106fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
1109 Ok(QueryMatch {
1110 node_id: m.node_id,
1111 span: m.span.start..m.span.end,
1112 content_span: if m.has_content_span != 0 {
1113 Some(m.content_span.start..m.content_span.end)
1114 } else {
1115 None
1116 },
1117 kind: borrowed_cstr(m.kind)?,
1118 })
1119}
1120
1121fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
1123 let node_id = |v: u32| if v == ffi::TWIG_NO_NODE { None } else { Some(NodeId(v)) };
1124 Ok(FlatNode {
1125 id: NodeId(n.id),
1126 parent: node_id(n.parent),
1127 first_child: node_id(n.first_child),
1128 next_sibling: node_id(n.next_sibling),
1129 span: n.span.start..n.span.end,
1130 content_span: if n.has_content_span != 0 {
1131 Some(n.content_span.start..n.content_span.end)
1132 } else {
1133 None
1134 },
1135 level: if n.level != 0 { Some(n.level) } else { None },
1136 kind: borrowed_cstr(n.kind)?,
1137 text: borrowed_bytes(n.text_ptr, n.text_len),
1138 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
1139 head: match n.head {
1140 ffi::TWIG_HEAD_NONE => None,
1141 v => Some(v != 0),
1142 },
1143 alignment: Alignment::from_c(n.alignment),
1144 name: borrowed_bytes(n.name_ptr, n.name_len),
1145 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
1146 })
1147}
1148
1149fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
1153 if ptr.is_null() || len == 0 {
1154 return Vec::new();
1155 }
1156 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
1157 kvs.iter()
1158 .map(|kv| {
1159 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
1160 (key, borrowed_bytes(kv.value, kv.value_len))
1161 })
1162 .collect()
1163}
1164
1165fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
1167 if ptr.is_null() {
1168 return Err(Error::Internal);
1169 }
1170 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
1171 .to_str()
1172 .map_err(|_| Error::Internal)?
1173 .to_owned())
1174}
1175
1176fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
1180 if ptr.is_null() {
1181 return None;
1182 }
1183 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1184 Some(String::from_utf8_lossy(bytes).into_owned())
1185}
1186
1187#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1191pub struct NodeId(pub u32);
1192
1193#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1196pub enum VoidKind {
1197 Doc,
1198 Para,
1199 ThematicBreak,
1200 Section,
1201 Div,
1202 BlockQuote,
1203 DefinitionList,
1204 Table,
1205 ListItem,
1206 DefinitionListItem,
1207 Term,
1208 Definition,
1209 Caption,
1210 SoftBreak,
1211 HardBreak,
1212 NonBreakingSpace,
1213 Emph,
1214 Strong,
1215 Span,
1216 Mark,
1217 Superscript,
1218 Subscript,
1219 Insert,
1220 Delete,
1221 DoubleQuoted,
1222 SingleQuoted,
1223}
1224
1225impl VoidKind {
1226 fn to_c(self) -> c_int {
1227 match self {
1229 VoidKind::Doc => 0,
1230 VoidKind::Para => 1,
1231 VoidKind::ThematicBreak => 3,
1232 VoidKind::Section => 4,
1233 VoidKind::Div => 5,
1234 VoidKind::BlockQuote => 9,
1235 VoidKind::DefinitionList => 13,
1236 VoidKind::Table => 14,
1237 VoidKind::ListItem => 15,
1238 VoidKind::DefinitionListItem => 17,
1239 VoidKind::Term => 18,
1240 VoidKind::Definition => 19,
1241 VoidKind::Caption => 22,
1242 VoidKind::SoftBreak => 26,
1243 VoidKind::HardBreak => 27,
1244 VoidKind::NonBreakingSpace => 28,
1245 VoidKind::Emph => 38,
1246 VoidKind::Strong => 39,
1247 VoidKind::Span => 42,
1248 VoidKind::Mark => 43,
1249 VoidKind::Superscript => 44,
1250 VoidKind::Subscript => 45,
1251 VoidKind::Insert => 46,
1252 VoidKind::Delete => 47,
1253 VoidKind::DoubleQuoted => 48,
1254 VoidKind::SingleQuoted => 49,
1255 }
1256 }
1257}
1258
1259#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1261pub enum TextKind {
1262 Str,
1263 Symb,
1264 Verbatim,
1265 InlineMath,
1266 DisplayMath,
1267 Url,
1268 Email,
1269 FootnoteReference,
1270 Comment,
1271 Doctype,
1272 Cdata,
1273}
1274
1275impl TextKind {
1276 fn to_c(self) -> c_int {
1277 match self {
1278 TextKind::Str => 25,
1279 TextKind::Symb => 29,
1280 TextKind::Verbatim => 30,
1281 TextKind::InlineMath => 32,
1282 TextKind::DisplayMath => 33,
1283 TextKind::Url => 34,
1284 TextKind::Email => 35,
1285 TextKind::FootnoteReference => 36,
1286 TextKind::Comment => 52,
1287 TextKind::Doctype => 53,
1288 TextKind::Cdata => 55,
1289 }
1290 }
1291}
1292
1293#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1295pub enum BulletStyle {
1296 Dash,
1297 Plus,
1298 Star,
1299}
1300
1301impl BulletStyle {
1302 fn to_c(self) -> c_int {
1303 match self {
1304 BulletStyle::Dash => 0,
1305 BulletStyle::Plus => 1,
1306 BulletStyle::Star => 2,
1307 }
1308 }
1309}
1310
1311#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1313pub enum OrderedNumbering {
1314 Decimal,
1315 LowerAlpha,
1316 UpperAlpha,
1317 LowerRoman,
1318 UpperRoman,
1319}
1320
1321impl OrderedNumbering {
1322 fn to_c(self) -> c_int {
1323 match self {
1324 OrderedNumbering::Decimal => 0,
1325 OrderedNumbering::LowerAlpha => 1,
1326 OrderedNumbering::UpperAlpha => 2,
1327 OrderedNumbering::LowerRoman => 3,
1328 OrderedNumbering::UpperRoman => 4,
1329 }
1330 }
1331}
1332
1333#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1335pub enum OrderedDelim {
1336 Period,
1337 ParenAfter,
1338 ParenBoth,
1339}
1340
1341impl OrderedDelim {
1342 fn to_c(self) -> c_int {
1343 match self {
1344 OrderedDelim::Period => 0,
1345 OrderedDelim::ParenAfter => 1,
1346 OrderedDelim::ParenBoth => 2,
1347 }
1348 }
1349}
1350
1351#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1354pub enum Alignment {
1355 Default,
1356 Left,
1357 Right,
1358 Center,
1359}
1360
1361impl Alignment {
1362 fn to_c(self) -> c_int {
1363 match self {
1364 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
1365 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
1366 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
1367 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
1368 }
1369 }
1370
1371 fn from_c(v: c_int) -> Option<Self> {
1374 match v {
1375 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
1376 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
1377 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
1378 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
1379 _ => None,
1380 }
1381 }
1382}
1383
1384#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1386pub enum SmartPunctuation {
1387 LeftSingleQuote,
1388 RightSingleQuote,
1389 LeftDoubleQuote,
1390 RightDoubleQuote,
1391 Ellipses,
1392 EmDash,
1393 EnDash,
1394}
1395
1396impl SmartPunctuation {
1397 fn to_c(self) -> c_int {
1398 match self {
1399 SmartPunctuation::LeftSingleQuote => 0,
1400 SmartPunctuation::RightSingleQuote => 1,
1401 SmartPunctuation::LeftDoubleQuote => 2,
1402 SmartPunctuation::RightDoubleQuote => 3,
1403 SmartPunctuation::Ellipses => 4,
1404 SmartPunctuation::EmDash => 5,
1405 SmartPunctuation::EnDash => 6,
1406 }
1407 }
1408}
1409
1410#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1412pub enum DirectiveForm {
1413 Text,
1414 Leaf,
1415 Container,
1416}
1417
1418impl DirectiveForm {
1419 fn to_c(self) -> c_int {
1420 match self {
1421 DirectiveForm::Text => 0,
1422 DirectiveForm::Leaf => 1,
1423 DirectiveForm::Container => 2,
1424 }
1425 }
1426}
1427
1428fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
1432 match s {
1433 Some(x) => (x.as_ptr(), x.len(), 1),
1434 None => (std::ptr::null(), 0, 0),
1435 }
1436}
1437
1438#[derive(Debug)]
1445pub struct Builder {
1446 raw: NonNull<ffi::TwigBuilder>,
1447}
1448
1449impl Builder {
1450 pub fn new() -> Result<Self, Error> {
1452 let mut raw = std::ptr::null_mut();
1453 let status = unsafe { ffi::twig_builder_create(&mut raw) };
1454 Error::from_status(status)?;
1455 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1456 Ok(Self { raw })
1457 }
1458
1459 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
1462 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
1463 }
1464
1465 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
1467 self.emit(|b, out| unsafe { ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out) })
1468 }
1469
1470 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
1472 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
1473 }
1474
1475 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
1477 let (lp, ll, has) = opt_str(lang);
1478 self.emit(|b, out| unsafe { ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out) })
1479 }
1480
1481 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1483 self.emit(|b, out| unsafe {
1484 ffi::twig_builder_add_raw_block(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1485 })
1486 }
1487
1488 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
1490 self.emit(|b, out| unsafe {
1491 ffi::twig_builder_add_metadata(b, lang.as_ptr(), lang.len(), text.as_ptr(), text.len(), out)
1492 })
1493 }
1494
1495 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
1497 self.emit(|b, out| unsafe {
1498 ffi::twig_builder_add_raw_inline(b, format.as_ptr(), format.len(), text.as_ptr(), text.len(), out)
1499 })
1500 }
1501
1502 pub fn add_smart_punctuation(&mut self, kind: SmartPunctuation, text: &str) -> Result<NodeId, Error> {
1504 self.emit(|b, out| unsafe {
1505 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
1506 })
1507 }
1508
1509 pub fn add_link(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1512 let (dp, dl, hd) = opt_str(destination);
1513 let (rp, rl, hr) = opt_str(reference);
1514 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
1515 }
1516
1517 pub fn add_image(&mut self, destination: Option<&str>, reference: Option<&str>) -> Result<NodeId, Error> {
1519 let (dp, dl, hd) = opt_str(destination);
1520 let (rp, rl, hr) = opt_str(reference);
1521 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
1522 }
1523
1524 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
1526 self.emit(|b, out| unsafe { ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out) })
1527 }
1528
1529 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
1531 self.emit(|b, out| unsafe { ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out) })
1532 }
1533
1534 pub fn add_processing_instruction(&mut self, target: &str, data: &str) -> Result<NodeId, Error> {
1536 self.emit(|b, out| unsafe {
1537 ffi::twig_builder_add_processing_instruction(b, target.as_ptr(), target.len(), data.as_ptr(), data.len(), out)
1538 })
1539 }
1540
1541 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
1543 self.emit(|b, out| unsafe { ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out) })
1544 }
1545
1546 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
1548 self.emit(|b, out| unsafe {
1549 ffi::twig_builder_add_reference(b, label.as_ptr(), label.len(), destination.as_ptr(), destination.len(), out)
1550 })
1551 }
1552
1553 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
1555 self.emit(|b, out| unsafe { ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out) })
1556 }
1557
1558 pub fn add_ordered_list(
1560 &mut self,
1561 numbering: OrderedNumbering,
1562 delim: OrderedDelim,
1563 tight: bool,
1564 start: Option<u32>,
1565 ) -> Result<NodeId, Error> {
1566 let (start_val, has_start) = match start {
1567 Some(s) => (s, 1),
1568 None => (0, 0),
1569 };
1570 self.emit(|b, out| unsafe {
1571 ffi::twig_builder_add_ordered_list(b, numbering.to_c(), delim.to_c(), tight as c_int, start_val, has_start, out)
1572 })
1573 }
1574
1575 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
1577 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
1578 }
1579
1580 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
1582 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list_item(b, checked as c_int, out) })
1583 }
1584
1585 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
1587 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
1588 }
1589
1590 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
1592 self.emit(|b, out| unsafe { ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out) })
1593 }
1594
1595 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
1598 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
1599 let status = unsafe { ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len()) };
1600 Error::from_status(status)
1601 }
1602
1603 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
1607 let kvs: Vec<ffi::TwigKeyVal> = attrs
1608 .iter()
1609 .map(|(k, v)| ffi::TwigKeyVal {
1610 key: k.as_ptr(),
1611 key_len: k.len(),
1612 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
1613 value_len: v.map_or(0, |s| s.len()),
1614 })
1615 .collect();
1616 let status = unsafe { ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len()) };
1617 Error::from_status(status)
1618 }
1619
1620 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1623 let raw = self.raw.as_ptr();
1624 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
1625 }
1626
1627 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
1631 let raw = self.raw.as_ptr();
1632 let ffi_format: ffi::TwigFormat = format.into();
1633 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_serialize(raw, root.0, ffi_format as i32, ptr, len) })
1634 }
1635
1636 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
1638 let raw = self.raw.as_ptr();
1639 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
1640 }
1641
1642 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1645 let raw = self.raw.as_ptr();
1646 collect_matches(|ptr, len| unsafe {
1647 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
1648 })
1649 }
1650
1651 fn emit(
1654 &mut self,
1655 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
1656 ) -> Result<NodeId, Error> {
1657 let mut id: u32 = 0;
1658 let status = call(self.raw.as_ptr(), &mut id);
1659 Error::from_status(status)?;
1660 Ok(NodeId(id))
1661 }
1662}
1663
1664impl Drop for Builder {
1665 fn drop(&mut self) {
1666 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
1667 }
1668}
1669
1670#[cfg(test)]
1671mod tests {
1672 use super::*;
1673
1674 #[test]
1675 fn abi_version_matches() {
1676 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
1680 }
1681
1682 #[test]
1683 fn parses_and_renders_markdown_html() {
1684 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1685 let html = doc.render_html().expect("render html");
1686 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
1687 }
1688
1689 #[test]
1690 fn parses_html_input() {
1691 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
1692 let html = doc.render_html().expect("render html");
1693 assert!(String::from_utf8_lossy(&html).contains("hi"));
1694 }
1695
1696 #[test]
1697 fn serialize_round_trips_and_cross_converts() {
1698 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
1699
1700 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
1701 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
1702
1703 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
1705 }
1706
1707 #[test]
1708 fn serialize_markdown_to_djot() {
1709 let mut doc =
1710 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
1711 let djot = doc.serialize(Format::Djot).expect("serialize djot");
1712 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
1713 }
1714
1715 #[test]
1716 fn ast_json_dumps_the_tree() {
1717 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
1718 let json = doc.ast_json().expect("ast json");
1719 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1720 }
1721
1722 #[test]
1723 fn query_finds_nodes_by_selector() {
1724 let source = "# One\n\n## Two\n";
1725 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1726 let matches = doc.query("heading").expect("query");
1727
1728 assert_eq!(matches.len(), 2);
1729 for m in &matches {
1730 assert_eq!(m.kind, "heading");
1731 assert!(m.span.start < m.span.end);
1732 }
1733 }
1734
1735 #[test]
1736 fn query_recovers_code_spans() {
1737 let source = "prose `code` more prose\n";
1738 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
1739 let matches = doc.query("verbatim").expect("query");
1740
1741 assert_eq!(matches.len(), 1);
1742 assert_eq!(&source[matches[0].span.clone()], "`code`");
1743 }
1744
1745 #[test]
1746 fn query_rejects_a_malformed_selector() {
1747 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
1748 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
1749 }
1750
1751 #[test]
1752 fn editor_edits_by_index_path() {
1753 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
1754 ed.replace_content("0.0", "bye").expect("replace_content");
1755 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
1756 }
1757
1758 #[test]
1759 fn flat_nodes_expose_element_name_and_attrs() {
1760 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
1764 let mut ed = Editor::new_ext(
1765 src.as_bytes(),
1766 Format::Markdown,
1767 MarkdownExtensions { html_elements: true, ..Default::default() },
1768 )
1769 .expect("editor");
1770 let nodes = ed.nodes().expect("nodes");
1771
1772 let source = nodes
1773 .iter()
1774 .find(|n| n.name.as_deref() == Some("source"))
1775 .expect("a <source> element node");
1776 assert_eq!(
1777 source.attrs,
1778 vec![
1779 ("media".to_string(), Some("(prefers-color-scheme: dark)".to_string())),
1780 ("srcset".to_string(), Some("d.svg".to_string())),
1781 ]
1782 );
1783
1784 let img = nodes.iter().find(|n| n.kind == "image").expect("an image node");
1787 assert!(img.name.is_none());
1788 assert_eq!(img.destination.as_deref(), Some("l.svg"));
1789
1790 let picture_kids_str = nodes.iter().find(|n| n.kind == "str");
1792 if let Some(s) = picture_kids_str {
1793 assert!(s.name.is_none() && s.attrs.is_empty());
1794 }
1795 }
1796
1797 #[test]
1798 fn editor_insert_child_and_delete() {
1799 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
1800 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1801 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
1802 ed.delete("0.1").expect("delete");
1803 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
1804 }
1805
1806 #[test]
1807 fn editor_edits_by_selector() {
1808 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1809 ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1810 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
1811 }
1812
1813 #[test]
1814 fn editor_locator_errors_are_distinct() {
1815 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
1816 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
1817 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
1818 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
1819 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
1821 }
1822
1823 #[test]
1824 fn editor_reparse_break_rolls_back() {
1825 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
1826 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
1827 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
1828 }
1829
1830 #[test]
1831 fn editor_leaf_content_is_not_editable() {
1832 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
1833 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
1834 }
1835
1836 #[test]
1837 fn editor_query_reflects_current_tree() {
1838 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
1839 ed.insert_child("0", 1, "<b/>").expect("insert_child");
1840 assert_eq!(ed.query("element").expect("query").len(), 3);
1842 let json = ed.ast_json().expect("ast_json");
1843 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
1844 }
1845
1846 #[test]
1849 fn editor_edit_range_types_backspaces_and_reports_change() {
1850 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
1851
1852 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
1854 assert_eq!(ed.source_str().unwrap(), "aXb\n");
1855 assert_eq!(c.old, 1..1);
1856 assert_eq!(c.new, 1..2);
1857 assert_eq!(c.delta(), 1);
1858
1859 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
1861 assert_eq!(ed.source_str().unwrap(), "ab\n");
1862 assert_eq!(c2.old, 1..2);
1863 assert_eq!(c2.new, 1..1);
1864 assert_eq!(c2.delta(), -1);
1865 }
1866
1867 #[test]
1868 fn editor_edit_range_rejects_bad_ranges() {
1869 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
1870 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"); }
1874
1875 #[test]
1876 fn editor_last_change_reports_locator_ops_too() {
1877 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
1878 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed").expect("replace");
1881 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
1882 let c = ed.last_change().expect("a change was recorded");
1883 assert_eq!(c.old, 7..13);
1885 assert_eq!(c.new, 7..17);
1886 }
1887
1888 #[test]
1889 fn editor_nodes_is_a_walkable_flat_tree() {
1890 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
1891 let nodes = ed.nodes().expect("nodes");
1892 assert!(!nodes.is_empty());
1893
1894 for (i, n) in nodes.iter().enumerate() {
1896 assert_eq!(n.id, NodeId(i as u32));
1897 }
1898 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
1900 assert_eq!(roots.len(), 1);
1901 assert_eq!(roots[0].kind, "doc");
1902
1903 let heading = nodes.iter().find(|n| n.kind == "heading").expect("a heading");
1905 assert_eq!(heading.level, Some(1));
1906 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
1907
1908 assert_eq!(heading.head, None);
1910 assert_eq!(heading.alignment, None);
1911
1912 for n in nodes.iter().filter(|n| n.parent.is_some()) {
1915 let p = &nodes[n.parent.unwrap().0 as usize];
1916 let mut kid = p.first_child;
1917 let mut seen = false;
1918 while let Some(NodeId(k)) = kid {
1919 if k == n.id.0 {
1920 seen = true;
1921 break;
1922 }
1923 kid = nodes[k as usize].next_sibling;
1924 }
1925 assert!(seen, "node {:?} not found among its parent's children", n.id);
1926 }
1927 }
1928
1929 #[test]
1930 fn editor_child_spans_and_subtree_agree_with_nodes() {
1931 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
1932 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
1933 let all = ed.nodes().expect("nodes");
1934 let doc = all.iter().find(|n| n.kind == "doc").expect("doc");
1935
1936 let top = ed.child_spans(None).expect("child_spans");
1939 let mut want = Vec::new();
1940 let mut c = doc.first_child;
1941 while let Some(id) = c {
1942 want.push(id);
1943 c = all[id.0 as usize].next_sibling;
1944 }
1945 assert_eq!(top.len(), want.len(), "top-level count");
1946 for (m, id) in top.iter().zip(&want) {
1947 assert_eq!(m.node_id, id.0, "child id");
1948 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
1949 assert_eq!(m.span, all[id.0 as usize].span, "child span");
1950 }
1951 assert!(src[top[0].span.clone()].starts_with('#'), "first block is the heading");
1953
1954 let list = top.iter().find(|m| m.kind.ends_with("list")).expect("a list");
1956 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
1957 assert_eq!(items.len(), 2);
1958 assert!(items.iter().all(|m| m.kind == "list_item"), "items: {items:?}");
1959
1960 let para = top.iter().find(|m| m.kind == "para").expect("a para").node_id;
1962 let sub = ed.subtree(NodeId(para)).expect("subtree");
1963 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
1964 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
1965 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
1966 assert_eq!(sub[0].kind, "para");
1967 for (i, n) in sub.iter().enumerate() {
1968 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
1969 for link in [n.parent, n.first_child, n.next_sibling].into_iter().flatten() {
1970 assert!((link.0 as usize) < sub.len(), "link {link:?} escapes the subtree");
1971 }
1972 }
1973 assert!(
1974 src[sub[0].span.clone()].starts_with("Hello"),
1975 "absolute span: {:?}",
1976 &src[sub[0].span.clone()]
1977 );
1978
1979 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<String> {
1981 let mut out = Vec::new();
1982 let mut stack = vec![root];
1983 while let Some(id) = stack.pop() {
1984 let n = &all[id.0 as usize];
1985 out.push(n.kind.clone());
1986 let mut c = n.first_child;
1987 while let Some(cid) = c {
1988 stack.push(cid);
1989 c = all[cid.0 as usize].next_sibling;
1990 }
1991 }
1992 out
1993 }
1994 let mut want_kinds = arena_kinds(&all, NodeId(para));
1995 let mut got_kinds: Vec<String> = sub.iter().map(|n| n.kind.clone()).collect();
1996 want_kinds.sort();
1997 got_kinds.sort();
1998 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
1999
2000 assert!(matches!(ed.subtree(NodeId(9999)), Err(Error::InvalidArgument)));
2002 }
2003
2004 #[test]
2005 fn flat_nodes_carry_table_head_and_alignment() {
2006 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
2010 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
2011 let nodes = ed.nodes().expect("nodes");
2012
2013 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == "row").collect();
2014 assert_eq!(rows.len(), 2, "a header row and one body row");
2015 assert_eq!(rows[0].head, Some(true), "first row is the header");
2016 assert_eq!(rows[1].head, Some(false), "second row is a body row");
2017
2018 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == "cell").collect();
2019 assert_eq!(cells.len(), 4);
2020 assert_eq!(cells[0].alignment, Some(Alignment::Left));
2022 assert_eq!(cells[1].alignment, Some(Alignment::Right));
2023 assert_eq!(cells[2].alignment, Some(Alignment::Left));
2024 assert_eq!(cells[3].alignment, Some(Alignment::Right));
2025 assert_eq!(cells[0].head, Some(true));
2027 assert_eq!(cells[2].head, Some(false));
2028
2029 let mut plain = Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
2032 let pnodes = plain.nodes().expect("nodes");
2033 let pcell = pnodes.iter().find(|n| n.kind == "cell").expect("a cell");
2034 assert_eq!(pcell.alignment, Some(Alignment::Default));
2035 }
2036
2037 #[test]
2038 fn editor_node_at_and_ancestors_hit_test_offsets() {
2039 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
2040
2041 let m = ed.node_at(2).expect("node_at").expect("a node covers offset 2");
2043 assert!(m.span.contains(&2));
2044
2045 let chain = ed.ancestors_at(2).expect("ancestors_at");
2047 assert!(!chain.is_empty());
2048 assert_eq!(chain[0].kind, "doc");
2049 assert_eq!(chain.last().unwrap().node_id, m.node_id);
2050
2051 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
2053 }
2054
2055 #[test]
2058 fn editor_wrap_and_toggle_inline_round_trip() {
2059 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
2060
2061 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
2063 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
2064 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
2065
2066 ed.toggle_inline(4, 8, InlineKind::Strong).expect("toggle off");
2068 assert_eq!(ed.source_str().unwrap(), "a word b\n");
2069
2070 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
2072 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
2073 }
2074
2075 #[test]
2076 fn editor_inline_kind_support_is_format_specific() {
2077 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
2079 assert_eq!(md.wrap_range(2, 6, InlineKind::Mark), Err(Error::UnsupportedFormat));
2080
2081 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
2083 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
2084 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
2085 }
2086
2087 #[test]
2088 fn editor_toggle_strips_verbatim_via_content_span() {
2089 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
2090 ed.toggle_inline(2, 8, InlineKind::Verbatim).expect("toggle code off");
2092 assert_eq!(ed.source_str().unwrap(), "a code b\n");
2093
2094 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
2097 ed2.toggle_inline(2, 7, InlineKind::Verbatim).expect("toggle multi off");
2098 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
2099 }
2100
2101 #[test]
2102 fn editor_set_block_switches_para_and_heading_levels() {
2103 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
2104
2105 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
2107 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
2108
2109 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
2111 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
2112
2113 ed.set_block(2, BlockKind::Paragraph).expect("to para");
2115 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
2116 }
2117
2118 #[test]
2119 fn editor_set_block_rejects_bad_level_and_format() {
2120 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2121 assert_eq!(md.set_block(0, BlockKind::Heading(9)), Err(Error::InvalidArgument));
2122
2123 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2124 assert_eq!(xml.set_block(1, BlockKind::Heading(1)), Err(Error::UnsupportedFormat));
2125 }
2126
2127 #[test]
2128 fn editor_toggle_block_container_round_trips() {
2129 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
2130
2131 let c = ed
2132 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
2133 .expect("quote on");
2134 assert_eq!(ed.source_str().unwrap(), "> a\n");
2135 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
2136
2137 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2138 .expect("quote off");
2139 assert_eq!(ed.source_str().unwrap(), "a\n");
2140 }
2141
2142 #[test]
2143 fn editor_toggle_block_container_nests_a_partial_selection() {
2144 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
2145
2146 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
2149 .expect("nest");
2150 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
2151
2152 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
2154 .expect("peel");
2155 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
2156 }
2157
2158 #[test]
2159 fn editor_toggle_block_container_numbers_and_converts_lists() {
2160 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
2161
2162 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
2164 .expect("ordered on");
2165 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
2166
2167 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
2169 .expect("convert");
2170 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
2171 }
2172
2173 #[test]
2174 fn editor_toggle_block_container_rejects_unspellable_format() {
2175 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2176 assert_eq!(
2177 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
2178 Err(Error::UnsupportedFormat)
2179 );
2180 }
2181
2182 #[test]
2183 fn editor_insert_link_wraps_and_repoints() {
2184 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
2185
2186 ed.insert_link(2, 6, "http://x.dev").expect("link");
2187 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
2188
2189 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
2191 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
2192 }
2193
2194 #[test]
2195 fn editor_insert_link_repoints_an_autolink() {
2196 for format in [Format::Markdown, Format::Djot] {
2201 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
2202 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
2203 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
2204
2205 let nodes = ed.nodes().expect("nodes");
2207 let url = nodes
2208 .iter()
2209 .find(|n| n.kind == "url")
2210 .expect("still an autolink");
2211 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
2212 assert!(!nodes.iter().any(|n| n.kind == "link"));
2213 }
2214 }
2215
2216 #[test]
2217 fn editor_insert_link_escapes_the_destination() {
2218 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
2221 dj.insert_link(0, 1, "a)b").expect("link");
2222 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
2223
2224 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
2228 md.insert_link(0, 1, "a b").expect("link");
2229 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
2230
2231 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
2232 dj2.insert_link(0, 1, "a b").expect("link");
2233 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
2234 }
2235
2236 #[test]
2237 fn editor_insert_link_rejects_a_newline_destination() {
2238 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
2239 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
2240
2241 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2242 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
2243 }
2244
2245 #[test]
2246 fn editor_insert_literal_keeps_typed_specials_literal() {
2247 for format in [Format::Markdown, Format::Djot] {
2248 let mut ed = Editor::new_str("z\n", format).expect("editor");
2249 ed.insert_literal(0, "*hi*").expect("literal");
2251
2252 let nodes = ed.nodes().expect("nodes");
2254 assert!(!nodes.iter().any(|n| n.kind == "emph" || n.kind == "strong"));
2255 let text: String = nodes
2256 .iter()
2257 .filter(|n| n.kind == "str")
2258 .filter_map(|n| n.text.clone())
2259 .collect();
2260 assert_eq!(text, "*hi*z");
2261 }
2262 }
2263
2264 #[test]
2265 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
2266 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
2268 ed.insert_literal(1, "# ").expect("literal");
2269 assert_eq!(ed.source_str().unwrap(), "a# z\n");
2270
2271 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
2273 ed2.insert_literal(0, "# ").expect("literal");
2274 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
2275 assert!(!ed2.nodes().expect("nodes").iter().any(|n| n.kind == "heading"));
2276 }
2277
2278 #[test]
2279 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
2280 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
2281 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
2282
2283 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
2284 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
2285 }
2286
2287 #[test]
2288 fn editor_insert_line_break_splices_in_cell_br() {
2289 let mut ed =
2290 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
2291 ed.insert_line_break(3).expect("line break");
2293 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
2294 let nodes = ed.nodes().expect("nodes");
2296 assert!(nodes.iter().any(|n| n.kind == "hard_break"));
2297 assert!(!nodes.iter().any(|n| n.kind == "raw_inline"));
2298 }
2299
2300 #[test]
2301 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
2302 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
2304 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
2305
2306 let mut dj =
2308 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
2309 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
2310
2311 let mut ed =
2313 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
2314 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
2315 }
2316
2317 #[test]
2318 fn editor_undo_redo_round_trip() {
2319 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2320 ed.edit_range(5, 5, "!").expect("edit");
2321 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2322
2323 let change = ed.undo().expect("undo ok").expect("something to undo");
2324 assert_eq!(ed.source_str().unwrap(), "hello\n");
2325 assert_eq!(change.new.end, 5);
2326 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
2327
2328 ed.redo().expect("redo ok").expect("something to redo");
2329 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2330 }
2331
2332 #[test]
2333 fn editor_coalesce_folds_a_run() {
2334 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2335 ed.edit_range(0, 0, "a").expect("edit");
2336 ed.edit_range(1, 1, "b").expect("edit");
2337 ed.coalesce_last_undo().expect("coalesce");
2338 assert_eq!(ed.source_str().unwrap(), "ab\n");
2339 ed.undo().expect("undo ok").expect("something to undo");
2341 assert_eq!(ed.source_str().unwrap(), "\n");
2342 assert!(ed.undo().expect("undo ok").is_none());
2343 }
2344
2345 #[test]
2346 fn editor_revision_bumps_per_successful_mutation() {
2347 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
2348 assert_eq!(ed.revision(), 0);
2349 ed.edit_range(1, 1, "y").expect("edit");
2350 assert_eq!(ed.revision(), 1);
2351
2352 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
2354 assert_eq!(xml.revision(), 0);
2355 assert!(xml.replace_content("0", "<b>").is_err());
2356 assert_eq!(xml.revision(), 0);
2357
2358 ed.undo().expect("undo ok").expect("something to undo");
2360 assert_eq!(ed.revision(), 2);
2361 ed.redo().expect("redo ok").expect("something to redo");
2362 assert_eq!(ed.revision(), 3);
2363 }
2364
2365 #[test]
2366 fn editor_dirty_range_tracks_and_clears() {
2367 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
2368 assert_eq!(ed.dirty_range(), None);
2370
2371 ed.edit_range(2, 2, "XY").expect("edit");
2373 assert_eq!(ed.dirty_range(), Some(2..4));
2374
2375 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
2379 assert!(d.start <= 2 && d.end >= 10, "range {d:?} must cover both edits");
2380
2381 let rev = ed.revision();
2383 ed.clear_dirty();
2384 assert_eq!(ed.dirty_range(), None);
2385 assert_eq!(ed.revision(), rev);
2386
2387 ed.undo().expect("undo ok").expect("something to undo");
2389 assert!(ed.dirty_range().is_some());
2390 }
2391
2392 #[test]
2393 fn editor_caret_blob_follows_undo_and_redo() {
2394 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
2395 assert!(ed.caret_blob().unwrap().is_empty());
2396
2397 ed.set_caret_blob(b"before").expect("set caret");
2399 ed.edit_range(5, 5, "!").expect("edit");
2400 assert!(ed.caret_blob().unwrap().is_empty());
2402 ed.set_caret_blob(b"after").expect("set caret");
2403
2404 ed.undo().expect("undo ok").expect("something to undo");
2406 assert_eq!(ed.source_str().unwrap(), "hello\n");
2407 assert_eq!(ed.caret_blob().unwrap(), b"before");
2408
2409 ed.redo().expect("redo ok").expect("something to redo");
2411 assert_eq!(ed.source_str().unwrap(), "hello!\n");
2412 assert_eq!(ed.caret_blob().unwrap(), b"after");
2413 }
2414
2415 #[test]
2416 fn editor_coalesced_run_keeps_the_pre_run_caret() {
2417 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
2418 ed.set_caret_blob(b"c0").expect("set caret");
2419 ed.edit_range(0, 0, "a").expect("edit");
2420 ed.set_caret_blob(b"c1").expect("set caret");
2421 ed.edit_range(1, 1, "b").expect("edit");
2422 ed.coalesce_last_undo().expect("coalesce");
2423 ed.set_caret_blob(b"c2").expect("set caret");
2424
2425 ed.undo().expect("undo ok").expect("something to undo");
2427 assert_eq!(ed.source_str().unwrap(), "\n");
2428 assert_eq!(ed.caret_blob().unwrap(), b"c0");
2429 }
2430
2431 #[test]
2432 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
2433 let mut ed =
2434 Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
2435 ed.renumber_ordered_lists(0).expect("renumber ok");
2436 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
2437 }
2438
2439 #[test]
2440 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
2441 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
2442 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
2443 }
2444
2445 #[test]
2446 fn editor_table_insert_row_and_set_alignment() {
2447 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
2448 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
2449 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
2451 ed.source_str().unwrap(),
2452 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
2453 );
2454 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
2456 }
2457
2458 #[test]
2459 fn editor_table_edit_off_a_table_is_not_found() {
2460 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
2461 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
2462 }
2463
2464 #[test]
2465 fn editor_set_block_converts_setext_heading() {
2466 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
2468 ed.set_block(0, BlockKind::Heading(1)).expect("setext to atx");
2469 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
2470 }
2471
2472 #[test]
2473 fn editor_unwrap_and_smart_delete() {
2474 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
2475 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
2477
2478 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
2479 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
2481 }
2482
2483 #[test]
2484 fn editor_directives_require_the_extension_flag() {
2485 let src = ":::vis{.public}\nhi\n:::\n";
2486 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
2489 assert_eq!(plain.query("directive").expect("query").len(), 0);
2490 let mut ext = Editor::new_ext(
2492 src.as_bytes(),
2493 Format::Markdown,
2494 MarkdownExtensions { directives: true, ..Default::default() },
2495 )
2496 .expect("editor");
2497 assert_eq!(ext.query("directive").expect("query").len(), 1);
2498 }
2499
2500 #[test]
2501 fn document_html_elements_make_embedded_img_queryable() {
2502 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
2503 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
2505 assert_eq!(plain.query("image").expect("query").len(), 0);
2506 let mut ext = Document::parse_str_with(
2508 src,
2509 Format::Markdown,
2510 MarkdownExtensions { html_elements: true, ..Default::default() },
2511 )
2512 .expect("parse");
2513 let images = ext.query("image").expect("query");
2514 assert_eq!(images.len(), 1);
2515 assert_eq!(images[0].kind, "image");
2516 }
2517
2518 #[test]
2519 fn editor_filter_public_audience_view() {
2520 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
2521 let mut ed = Editor::new_ext(
2522 src.as_bytes(),
2523 Format::Markdown,
2524 MarkdownExtensions { directives: true, ..Default::default() },
2525 )
2526 .expect("editor");
2527 ed.filter(
2529 "directive[name=vis]",
2530 Some("directive[class~=public]"),
2531 true,
2532 )
2533 .expect("filter");
2534 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
2535 }
2536
2537 #[test]
2538 fn editor_filter_rejects_a_malformed_selector() {
2539 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
2540 assert_eq!(ed.filter("list >", None, false), Err(Error::InvalidArgument));
2541 }
2542
2543 #[test]
2544 fn builder_builds_and_renders_a_document() {
2545 let mut b = Builder::new().expect("builder");
2546
2547 let title = b.add_text(TextKind::Str, "Title").unwrap();
2549 let heading = b.add_heading(1).unwrap();
2550 b.set_children(heading, &[title]).unwrap();
2551
2552 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
2553 let world = b.add_text(TextKind::Str, "world").unwrap();
2554 let emph = b.add(VoidKind::Emph).unwrap();
2555 b.set_children(emph, &[world]).unwrap();
2556 let para = b.add(VoidKind::Para).unwrap();
2557 b.set_children(para, &[hello, emph]).unwrap();
2558
2559 let doc = b.add(VoidKind::Doc).unwrap();
2560 b.set_children(doc, &[heading, para]).unwrap();
2561
2562 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
2563 assert!(html.contains("<h1>Title</h1>"), "{html}");
2564 assert!(html.contains("<em>world</em>"), "{html}");
2565
2566 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
2567 assert!(md.contains("# Title"), "{md}");
2568 assert!(md.contains("*world*"), "{md}");
2569
2570 let matches = b.query(doc, "heading").unwrap();
2571 assert_eq!(matches.len(), 1);
2572 assert_eq!(matches[0].kind, "heading");
2573
2574 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
2575 assert!(json.contains("\"kind\": \"doc\""), "{json}");
2576 }
2577
2578 #[test]
2579 fn builder_element_with_attributes() {
2580 let mut b = Builder::new().expect("builder");
2581 let inner = b.add_text(TextKind::Str, "hi").unwrap();
2582 let el = b.add_element("section").unwrap();
2583 b.set_children(el, &[inner]).unwrap();
2584 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)]).unwrap();
2585
2586 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
2587 assert!(html.contains("<section"), "{html}");
2588 assert!(html.contains("class=\"note\""), "{html}");
2589 assert!(html.contains("hidden"), "{html}");
2590 }
2591
2592 #[test]
2593 fn builder_lists_round_trip_to_markdown() {
2594 let mut b = Builder::new().expect("builder");
2595
2596 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
2598 let one_para = b.add(VoidKind::Para).unwrap();
2599 b.set_children(one_para, &[one_txt]).unwrap();
2600 let one = b.add(VoidKind::ListItem).unwrap();
2601 b.set_children(one, &[one_para]).unwrap();
2602
2603 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
2604 let two_para = b.add(VoidKind::Para).unwrap();
2605 b.set_children(two_para, &[two_txt]).unwrap();
2606 let two = b.add(VoidKind::ListItem).unwrap();
2607 b.set_children(two, &[two_para]).unwrap();
2608
2609 let list = b
2610 .add_ordered_list(OrderedNumbering::Decimal, OrderedDelim::Period, true, Some(1))
2611 .unwrap();
2612 b.set_children(list, &[one, two]).unwrap();
2613 let doc = b.add(VoidKind::Doc).unwrap();
2614 b.set_children(doc, &[list]).unwrap();
2615
2616 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
2617 assert!(md.contains("1. one"), "{md}");
2618 assert!(md.contains("2. two"), "{md}");
2619 }
2620
2621 #[test]
2622 fn builder_rejects_invalid_kind_and_id() {
2623 let b = Builder::new().expect("builder");
2624 let mut id = 0u32;
2628 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
2629 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2630
2631 let mut ptr = std::ptr::null();
2633 let mut len = 0usize;
2634 let status = unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
2635 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
2636 }
2637}