1mod error;
2
3pub(crate) use twig_sys as ffi;
8
9use std::marker::PhantomData;
10use std::ops::Range;
11use std::os::raw::{c_char, c_int};
12use std::ptr::NonNull;
13
14pub use error::Error;
15pub use ffi::TwigSpan as Span;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25#[non_exhaustive]
26pub enum Format {
27 Djot,
28 Markdown,
29 Xml,
30 Html,
31 Asciidoc,
41}
42
43impl From<Format> for ffi::TwigFormat {
44 fn from(value: Format) -> Self {
45 match value {
46 Format::Djot => ffi::TwigFormat::Djot,
47 Format::Markdown => ffi::TwigFormat::Markdown,
48 Format::Xml => ffi::TwigFormat::Xml,
49 Format::Html => ffi::TwigFormat::Html,
50 Format::Asciidoc => ffi::TwigFormat::Asciidoc,
51 }
52 }
53}
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70#[non_exhaustive]
71pub enum Target {
72 Djot,
73 Markdown,
74 Xml,
75 Html,
76 Asciidoc,
82}
83
84impl Target {
85 pub fn as_format(self) -> Option<Format> {
92 match self {
93 Target::Djot => Some(Format::Djot),
94 Target::Markdown => Some(Format::Markdown),
95 Target::Xml => Some(Format::Xml),
96 Target::Html => Some(Format::Html),
97 Target::Asciidoc => Some(Format::Asciidoc),
98 }
99 }
100}
101
102impl From<Format> for Target {
106 fn from(value: Format) -> Self {
107 match value {
108 Format::Djot => Target::Djot,
109 Format::Markdown => Target::Markdown,
110 Format::Xml => Target::Xml,
111 Format::Html => Target::Html,
112 Format::Asciidoc => Target::Asciidoc,
113 }
114 }
115}
116
117impl From<Target> for ffi::TwigFormat {
118 fn from(value: Target) -> Self {
119 match value {
120 Target::Djot => ffi::TwigFormat::Djot,
121 Target::Markdown => ffi::TwigFormat::Markdown,
122 Target::Xml => ffi::TwigFormat::Xml,
123 Target::Html => ffi::TwigFormat::Html,
124 Target::Asciidoc => ffi::TwigFormat::Asciidoc,
125 }
126 }
127}
128
129#[derive(Clone, Debug, Eq, PartialEq, Hash)]
163#[non_exhaustive]
164pub enum Kind {
165 Doc,
167 Para,
169 Heading,
170 ThematicBreak,
171 Section,
172 CodeBlock,
173 RawBlock,
174 Metadata,
175 BlockQuote,
176 BulletList,
177 OrderedList,
178 TaskList,
179 DefinitionList,
180 LineBlock,
181 Table,
182 ListItem,
184 TaskListItem,
185 DefinitionListItem,
186 Term,
187 Definition,
188 Line,
189 Row,
190 Cell,
191 Column,
192 Caption,
193 Footnote,
194 Reference,
195 Citation,
196 Substitution,
197 Str,
199 SoftBreak,
200 HardBreak,
201 NonBreakingSpace,
202 RawInline,
203 SmartPunctuation,
204 Link,
205 Image,
206 Emph,
208 Strong,
209 Mark,
210 Superscript,
211 Subscript,
212 Insert,
213 Delete,
214 DoubleQuoted,
215 SingleQuoted,
216 Symb,
218 Verbatim,
219 InlineMath,
220 DisplayMath,
221 Url,
222 Email,
223 FootnoteReference,
224 CitationReference,
225 SubstitutionReference,
226 Container,
228 ProcessingInstruction,
229 Comment,
230 Doctype,
231 Cdata,
232 Other(String),
239}
240
241impl Kind {
242 pub fn as_str(&self) -> &str {
245 match self {
246 Kind::Doc => "doc",
247 Kind::Para => "para",
248 Kind::Heading => "heading",
249 Kind::ThematicBreak => "thematic_break",
250 Kind::Section => "section",
251 Kind::CodeBlock => "code_block",
252 Kind::RawBlock => "raw_block",
253 Kind::Metadata => "metadata",
254 Kind::BlockQuote => "block_quote",
255 Kind::BulletList => "bullet_list",
256 Kind::OrderedList => "ordered_list",
257 Kind::TaskList => "task_list",
258 Kind::DefinitionList => "definition_list",
259 Kind::LineBlock => "line_block",
260 Kind::Table => "table",
261 Kind::ListItem => "list_item",
262 Kind::TaskListItem => "task_list_item",
263 Kind::DefinitionListItem => "definition_list_item",
264 Kind::Term => "term",
265 Kind::Definition => "definition",
266 Kind::Line => "line",
267 Kind::Row => "row",
268 Kind::Cell => "cell",
269 Kind::Column => "column",
270 Kind::Caption => "caption",
271 Kind::Footnote => "footnote",
272 Kind::Reference => "reference",
273 Kind::Citation => "citation",
274 Kind::Substitution => "substitution",
275 Kind::Str => "str",
276 Kind::SoftBreak => "soft_break",
277 Kind::HardBreak => "hard_break",
278 Kind::NonBreakingSpace => "non_breaking_space",
279 Kind::RawInline => "raw_inline",
280 Kind::SmartPunctuation => "smart_punctuation",
281 Kind::Link => "link",
282 Kind::Image => "image",
283 Kind::Container => "container",
284 Kind::ProcessingInstruction => "processing_instruction",
285 Kind::Emph => "emph",
286 Kind::Strong => "strong",
287 Kind::Mark => "mark",
288 Kind::Superscript => "superscript",
289 Kind::Subscript => "subscript",
290 Kind::Insert => "insert",
291 Kind::Delete => "delete",
292 Kind::DoubleQuoted => "double_quoted",
293 Kind::SingleQuoted => "single_quoted",
294 Kind::Symb => "symb",
295 Kind::Verbatim => "verbatim",
296 Kind::InlineMath => "inline_math",
297 Kind::DisplayMath => "display_math",
298 Kind::Url => "url",
299 Kind::Email => "email",
300 Kind::FootnoteReference => "footnote_reference",
301 Kind::CitationReference => "citation_reference",
302 Kind::SubstitutionReference => "substitution_reference",
303 Kind::Comment => "comment",
304 Kind::Doctype => "doctype",
305 Kind::Cdata => "cdata",
306 Kind::Other(name) => name.as_str(),
307 }
308 }
309
310 pub fn is_unknown(&self) -> bool {
314 matches!(self, Kind::Other(_))
315 }
316}
317
318impl From<&str> for Kind {
319 fn from(name: &str) -> Self {
320 match name {
321 "doc" => Kind::Doc,
322 "para" => Kind::Para,
323 "heading" => Kind::Heading,
324 "thematic_break" => Kind::ThematicBreak,
325 "section" => Kind::Section,
326 "code_block" => Kind::CodeBlock,
327 "raw_block" => Kind::RawBlock,
328 "metadata" => Kind::Metadata,
329 "block_quote" => Kind::BlockQuote,
330 "bullet_list" => Kind::BulletList,
331 "ordered_list" => Kind::OrderedList,
332 "task_list" => Kind::TaskList,
333 "definition_list" => Kind::DefinitionList,
334 "line_block" => Kind::LineBlock,
335 "table" => Kind::Table,
336 "list_item" => Kind::ListItem,
337 "task_list_item" => Kind::TaskListItem,
338 "definition_list_item" => Kind::DefinitionListItem,
339 "term" => Kind::Term,
340 "definition" => Kind::Definition,
341 "line" => Kind::Line,
342 "row" => Kind::Row,
343 "cell" => Kind::Cell,
344 "column" => Kind::Column,
345 "caption" => Kind::Caption,
346 "footnote" => Kind::Footnote,
347 "reference" => Kind::Reference,
348 "citation" => Kind::Citation,
349 "substitution" => Kind::Substitution,
350 "str" => Kind::Str,
351 "soft_break" => Kind::SoftBreak,
352 "hard_break" => Kind::HardBreak,
353 "non_breaking_space" => Kind::NonBreakingSpace,
354 "raw_inline" => Kind::RawInline,
355 "smart_punctuation" => Kind::SmartPunctuation,
356 "link" => Kind::Link,
357 "image" => Kind::Image,
358 "container" => Kind::Container,
359 "processing_instruction" => Kind::ProcessingInstruction,
360 "emph" => Kind::Emph,
361 "strong" => Kind::Strong,
362 "mark" => Kind::Mark,
363 "superscript" => Kind::Superscript,
364 "subscript" => Kind::Subscript,
365 "insert" => Kind::Insert,
366 "delete" => Kind::Delete,
367 "double_quoted" => Kind::DoubleQuoted,
368 "single_quoted" => Kind::SingleQuoted,
369 "symb" => Kind::Symb,
370 "verbatim" => Kind::Verbatim,
371 "inline_math" => Kind::InlineMath,
372 "display_math" => Kind::DisplayMath,
373 "url" => Kind::Url,
374 "email" => Kind::Email,
375 "footnote_reference" => Kind::FootnoteReference,
376 "citation_reference" => Kind::CitationReference,
377 "substitution_reference" => Kind::SubstitutionReference,
378 "comment" => Kind::Comment,
379 "doctype" => Kind::Doctype,
380 "cdata" => Kind::Cdata,
381 other => Kind::Other(other.to_string()),
382 }
383 }
384}
385
386impl std::fmt::Display for Kind {
387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 f.write_str(self.as_str())
389 }
390}
391
392#[derive(Clone, Debug, Eq, PartialEq)]
394pub struct QueryMatch {
395 pub node_id: u32,
397 pub span: Range<usize>,
399 pub content_span: Option<Range<usize>>,
402 pub kind: Kind,
405}
406
407#[derive(Clone, Debug, Eq, PartialEq)]
414pub struct Change {
415 pub old: Range<usize>,
416 pub new: Range<usize>,
417}
418
419impl Change {
420 pub fn delta(&self) -> isize {
422 self.new.len() as isize - self.old.len() as isize
423 }
424
425 fn from_ffi(c: ffi::TwigChange) -> Self {
426 Change {
427 old: c.old_span.start..c.old_span.end,
428 new: c.new_span.start..c.new_span.end,
429 }
430 }
431}
432
433#[derive(Clone, Debug, Eq, PartialEq)]
444#[non_exhaustive]
445pub struct FlatNode {
446 pub id: NodeId,
447 pub parent: Option<NodeId>,
448 pub first_child: Option<NodeId>,
449 pub next_sibling: Option<NodeId>,
450 pub span: Range<usize>,
451 pub content_span: Option<Range<usize>>,
452 pub level: Option<u32>,
454 pub kind: Kind,
455 pub text: Option<String>,
456 pub destination: Option<String>,
457 pub head: Option<bool>,
460 pub alignment: Option<Alignment>,
466 pub name: Option<String>,
478 pub directive_form: Option<DirectiveForm>,
493 pub origin: Option<ContainerOrigin>,
503 pub marker_span: Option<Range<usize>>,
522 pub checked: Option<bool>,
535 pub attrs: Vec<(String, Option<String>)>,
539}
540
541#[derive(Clone, Debug, Default, Eq, PartialEq)]
549pub struct LinePrefix {
550 pub text: String,
552 pub columns: usize,
554}
555
556#[derive(Clone, Copy, Debug, Eq, PartialEq)]
561pub enum InlineKind {
562 Strong,
563 Emph,
564 Verbatim,
565 Mark,
566 Superscript,
567 Subscript,
568 Insert,
569 Delete,
570}
571
572impl InlineKind {
573 fn to_c(self) -> c_int {
574 match self {
575 InlineKind::Strong => 0,
576 InlineKind::Emph => 1,
577 InlineKind::Verbatim => 2,
578 InlineKind::Mark => 3,
579 InlineKind::Superscript => 4,
580 InlineKind::Subscript => 5,
581 InlineKind::Insert => 6,
582 InlineKind::Delete => 7,
583 }
584 }
585}
586
587#[derive(Clone, Copy, Debug, Eq, PartialEq)]
589pub enum BlockKind {
590 Paragraph,
591 Heading(u32),
593}
594
595impl BlockKind {
596 fn to_c(self) -> (c_int, u32) {
598 match self {
599 BlockKind::Paragraph => (0, 0),
600 BlockKind::Heading(level) => (1, level),
601 }
602 }
603}
604
605#[derive(Clone, Copy, Debug, Eq, PartialEq)]
611pub enum BlockContainerKind {
612 BlockQuote,
613 BulletList,
614 OrderedList,
615}
616
617impl BlockContainerKind {
618 fn to_c(self) -> c_int {
619 match self {
620 BlockContainerKind::BlockQuote => 0,
621 BlockContainerKind::BulletList => 1,
622 BlockContainerKind::OrderedList => 2,
623 }
624 }
625}
626
627#[derive(Clone, Copy, Debug, Eq, PartialEq)]
628pub struct Version {
629 pub major: u8,
630 pub minor: u8,
631 pub patch: u8,
632}
633
634pub fn version() -> Version {
635 let packed = unsafe { ffi::twig_version() };
636 Version {
637 major: (packed >> 16) as u8,
638 minor: (packed >> 8) as u8,
639 patch: packed as u8,
640 }
641}
642
643pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
649
650pub fn abi_version() -> u32 {
656 unsafe { ffi::twig_abi_version() }
657}
658
659pub fn version_string() -> &'static str {
660 let ptr = unsafe { ffi::twig_version_string() };
661 unsafe { std::ffi::CStr::from_ptr(ptr) }
662 .to_str()
663 .unwrap_or("")
664}
665
666#[derive(Debug)]
667pub struct Document {
668 raw: NonNull<ffi::TwigDocument>,
669}
670
671impl Document {
672 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
673 Self::parse_with(input, format, MarkdownExtensions::default())
674 }
675
676 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
677 Self::parse(input.as_bytes(), format)
678 }
679
680 pub fn parse_with(
686 input: &[u8],
687 format: Format,
688 extensions: MarkdownExtensions,
689 ) -> Result<Self, Error> {
690 let mut raw = std::ptr::null_mut();
691 let ffi_format: ffi::TwigFormat = format.into();
692 let status = unsafe {
693 ffi::twig_parse_ext(
694 input.as_ptr(),
695 input.len(),
696 ffi_format as i32,
697 extensions.to_flags(),
698 &mut raw,
699 )
700 };
701 Error::from_status(status)?;
702 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
703 Ok(Self { raw })
704 }
705
706 pub fn parse_str_with(
708 input: &str,
709 format: Format,
710 extensions: MarkdownExtensions,
711 ) -> Result<Self, Error> {
712 Self::parse_with(input.as_bytes(), format, extensions)
713 }
714
715 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
718 let raw = self.raw.as_ptr();
719 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
720 }
721
722 pub fn serialize_to(&mut self, target: Target) -> Result<Vec<u8>, Error> {
733 let raw = self.raw.as_ptr();
734 let ffi_target: ffi::TwigFormat = target.into();
735 collect_bytes(|ptr, len| unsafe {
736 ffi::twig_document_serialize(raw, ffi_target as i32, ptr, len)
737 })
738 }
739
740 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
747 self.serialize_to(format.into())
748 }
749
750 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
753 let raw = self.raw.as_ptr();
754 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
755 }
756
757 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
766 let raw = self.raw.as_ptr();
767 collect_matches(|ptr, len| unsafe {
768 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
769 })
770 }
771
772 pub fn span(&mut self, node: NodeId) -> Result<Range<usize>, Error> {
774 let mut span = ffi::TwigSpan { start: 0, end: 0 };
775 let status = unsafe { ffi::twig_document_node_span(self.raw.as_ptr(), node.0, &mut span) };
776 Error::from_status(status)?;
777 Ok(span.start..span.end)
778 }
779
780 pub fn content_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
783 let mut span = ffi::TwigSpan { start: 0, end: 0 };
784 let status =
785 unsafe { ffi::twig_document_node_content_span(self.raw.as_ptr(), node.0, &mut span) };
786 match status.0 {
787 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
788 ffi::TwigStatus::NOT_FOUND => Ok(None),
789 _ => Err(Error::from_status(status).unwrap_err()),
790 }
791 }
792
793 pub fn marker_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
797 let mut span = ffi::TwigSpan { start: 0, end: 0 };
798 let status =
799 unsafe { ffi::twig_document_node_marker_span(self.raw.as_ptr(), node.0, &mut span) };
800 match status.0 {
801 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
802 ffi::TwigStatus::NOT_FOUND => Ok(None),
803 _ => Err(Error::from_status(status).unwrap_err()),
804 }
805 }
806
807 pub fn line_prefix(&mut self, offset: usize) -> Result<Option<Range<usize>>, Error> {
824 let mut span = ffi::TwigSpan { start: 0, end: 0 };
825 let status =
826 unsafe { ffi::twig_document_line_prefix(self.raw.as_ptr(), offset, &mut span) };
827 match status.0 {
828 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
829 ffi::TwigStatus::NOT_FOUND => Ok(None),
830 _ => Err(Error::from_status(status).unwrap_err()),
831 }
832 }
833
834 pub fn continuation_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
860 self.prefix_via(offset, ffi::twig_document_continuation_prefix)
861 }
862
863 pub fn blank_line_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
875 self.prefix_via(offset, ffi::twig_document_blank_line_prefix)
876 }
877
878 fn prefix_via(
880 &mut self,
881 offset: usize,
882 f: unsafe extern "C" fn(
883 *mut ffi::TwigDocument,
884 usize,
885 *mut *const u8,
886 *mut usize,
887 *mut usize,
888 ) -> ffi::TwigStatus,
889 ) -> Result<LinePrefix, Error> {
890 let mut ptr: *const u8 = std::ptr::null();
891 let mut len = 0usize;
892 let mut columns = 0usize;
893 let status = unsafe { f(self.raw.as_ptr(), offset, &mut ptr, &mut len, &mut columns) };
894 Error::from_status(status)?;
895 let text = if ptr.is_null() || len == 0 {
896 String::new()
897 } else {
898 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
899 String::from_utf8(bytes.to_vec()).map_err(|_| Error::Internal)?
900 };
901 Ok(LinePrefix { text, columns })
902 }
903
904 pub fn cell_extent(&mut self, node: NodeId) -> Result<Option<(u32, u32)>, Error> {
916 let raw = self.raw.as_ptr();
917 let mut colspan: u32 = 0;
918 let status = unsafe { ffi::twig_document_cell_colspan(raw, node.0, &mut colspan) };
919 match status.0 {
920 ffi::TwigStatus::OK => {}
921 ffi::TwigStatus::NOT_FOUND => return Ok(None),
922 _ => return Err(Error::from_status(status).unwrap_err()),
923 }
924 let mut rowspan: u32 = 0;
925 Error::from_status(unsafe { ffi::twig_document_cell_rowspan(raw, node.0, &mut rowspan) })?;
926 Ok(Some((colspan, rowspan)))
927 }
928
929 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
934 let raw = self.raw.as_ptr();
935 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_nodes(raw, ptr, len) })
936 }
937
938 pub fn definitions(&mut self) -> Result<Vec<QueryMatch>, Error> {
955 let raw = self.raw.as_ptr();
956 collect_matches(|ptr, len| unsafe { ffi::twig_document_definitions(raw, ptr, len) })
957 }
958
959 pub fn diagnostics(&mut self, target: Target) -> Result<Vec<Warning>, Error> {
979 let raw = self.raw.as_ptr();
980 let code = ffi::TwigFormat::from(target) as c_int;
981 let mut ptr: *const ffi::TwigWarning = std::ptr::null();
982 let mut len = 0usize;
983 let status = unsafe { ffi::twig_document_diagnostics(raw, code, &mut ptr, &mut len) };
984 Error::from_status(status)?;
985 if len == 0 || ptr.is_null() {
986 return Ok(Vec::new());
987 }
988 let raw_warnings = unsafe { std::slice::from_raw_parts(ptr, len) };
989 Ok(raw_warnings
990 .iter()
991 .map(|w| Warning {
992 fidelity: Fidelity::from_c(w.fidelity),
993 path: borrowed_bytes(w.path_ptr, w.path_len).unwrap_or_default(),
994 kind: Kind::from(borrowed_cstr(w.kind).unwrap_or_default().as_str()),
995 })
996 .collect())
997 }
998
999 pub fn children(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1005 let raw = self.raw.as_ptr();
1006 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1007 collect_matches(|ptr, len| unsafe { ffi::twig_document_children(raw, id, ptr, len) })
1008 }
1009
1010 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1016 let raw = self.raw.as_ptr();
1017 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_subtree(raw, node.0, ptr, len) })
1018 }
1019
1020 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1025 let mut m = empty_ffi_match();
1026 let status = unsafe { ffi::twig_document_node_at(self.raw.as_ptr(), offset, &mut m) };
1027 match status.0 {
1028 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1029 ffi::TwigStatus::NOT_FOUND => Ok(None),
1030 _ => Err(Error::from_status(status).unwrap_err()),
1031 }
1032 }
1033
1034 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1038 let raw = self.raw.as_ptr();
1039 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1040 let mut len = 0usize;
1041 let status = unsafe { ffi::twig_document_nodes_at(raw, offset, &mut ptr, &mut len) };
1042 match status.0 {
1043 ffi::TwigStatus::OK => {}
1044 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1045 _ => return Err(Error::from_status(status).unwrap_err()),
1046 }
1047 if len == 0 || ptr.is_null() {
1048 return Ok(Vec::new());
1049 }
1050 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1051 raw_matches.iter().map(query_match_from_ffi).collect()
1052 }
1053
1054 pub fn node_at_caret(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1077 let mut m = empty_ffi_match();
1078 let status = unsafe { ffi::twig_document_node_at_caret(self.raw.as_ptr(), offset, &mut m) };
1079 match status.0 {
1080 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1081 ffi::TwigStatus::NOT_FOUND => Ok(None),
1082 _ => Err(Error::from_status(status).unwrap_err()),
1083 }
1084 }
1085
1086 pub fn ancestors_at_caret(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1090 let raw = self.raw.as_ptr();
1091 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1092 let mut len = 0usize;
1093 let status = unsafe { ffi::twig_document_nodes_at_caret(raw, offset, &mut ptr, &mut len) };
1094 match status.0 {
1095 ffi::TwigStatus::OK => {}
1096 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1097 _ => return Err(Error::from_status(status).unwrap_err()),
1098 }
1099 if len == 0 || ptr.is_null() {
1100 return Ok(Vec::new());
1101 }
1102 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1103 raw_matches.iter().map(query_match_from_ffi).collect()
1104 }
1105}
1106
1107#[derive(Debug)]
1118pub struct DocumentView<'a> {
1119 doc: Document,
1120 _editor: PhantomData<&'a mut Editor>,
1121}
1122
1123impl std::ops::Deref for DocumentView<'_> {
1124 type Target = Document;
1125
1126 fn deref(&self) -> &Document {
1127 &self.doc
1128 }
1129}
1130
1131impl std::ops::DerefMut for DocumentView<'_> {
1132 fn deref_mut(&mut self) -> &mut Document {
1133 &mut self.doc
1134 }
1135}
1136
1137impl Drop for Document {
1138 fn drop(&mut self) {
1139 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
1140 }
1141}
1142
1143#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1149pub struct MarkdownExtensions {
1150 pub directives: bool,
1152 pub math: bool,
1154 pub html_elements: bool,
1159}
1160
1161impl MarkdownExtensions {
1162 fn to_flags(self) -> u32 {
1163 let mut flags = 0;
1164 if self.directives {
1165 flags |= ffi::TWIG_MD_DIRECTIVES;
1166 }
1167 if self.math {
1168 flags |= ffi::TWIG_MD_MATH;
1169 }
1170 if self.html_elements {
1171 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
1172 }
1173 flags
1174 }
1175}
1176
1177#[derive(Debug)]
1183pub struct Editor {
1184 raw: NonNull<ffi::TwigEditor>,
1185}
1186
1187impl Editor {
1188 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
1191 let mut raw = std::ptr::null_mut();
1192 let ffi_format: ffi::TwigFormat = format.into();
1193 let status = unsafe {
1194 ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw)
1195 };
1196 Error::from_status(status)?;
1197 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1198 Ok(Self { raw })
1199 }
1200
1201 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
1202 Self::new(input.as_bytes(), format)
1203 }
1204
1205 pub fn new_ext(
1210 input: &[u8],
1211 format: Format,
1212 extensions: MarkdownExtensions,
1213 ) -> Result<Self, Error> {
1214 let mut raw = std::ptr::null_mut();
1215 let ffi_format: ffi::TwigFormat = format.into();
1216 let status = unsafe {
1217 ffi::twig_editor_create_ext(
1218 input.as_ptr(),
1219 input.len(),
1220 ffi_format as i32,
1221 extensions.to_flags(),
1222 &mut raw,
1223 )
1224 };
1225 Error::from_status(status)?;
1226 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1227 Ok(Self { raw })
1228 }
1229
1230 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1232 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1233 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
1234 })
1235 }
1236
1237 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1240 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1241 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
1242 })
1243 }
1244
1245 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1247 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1248 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
1249 })
1250 }
1251
1252 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1254 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1255 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
1256 })
1257 }
1258
1259 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
1262 let status = unsafe {
1263 ffi::twig_editor_insert_child(
1264 self.raw.as_ptr(),
1265 locator.as_ptr(),
1266 locator.len(),
1267 index,
1268 text.as_ptr(),
1269 text.len(),
1270 )
1271 };
1272 Error::from_status(status)
1273 }
1274
1275 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
1278 let status =
1279 unsafe { ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1280 Error::from_status(status)
1281 }
1282
1283 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
1286 let status = unsafe {
1287 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
1288 };
1289 Error::from_status(status)
1290 }
1291
1292 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
1296 let status =
1297 unsafe { ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1298 Error::from_status(status)
1299 }
1300
1301 pub fn filter(
1306 &mut self,
1307 drop: &str,
1308 keep: Option<&str>,
1309 unwrap_kept: bool,
1310 ) -> Result<(), Error> {
1311 let (keep_ptr, keep_len) = match keep {
1312 Some(k) => (k.as_ptr(), k.len()),
1313 None => (std::ptr::null(), 0),
1314 };
1315 let status = unsafe {
1316 ffi::twig_editor_filter(
1317 self.raw.as_ptr(),
1318 drop.as_ptr(),
1319 drop.len(),
1320 keep_ptr,
1321 keep_len,
1322 unwrap_kept as i32,
1323 )
1324 };
1325 Error::from_status(status)
1326 }
1327
1328 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
1330 let raw = self.raw.as_ptr();
1331 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
1332 }
1333
1334 pub fn source_str(&mut self) -> Result<String, Error> {
1336 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
1337 }
1338
1339 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
1342 let raw = self.raw.as_ptr();
1343 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
1344 }
1345
1346 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1349 let raw = self.raw.as_ptr();
1350 collect_matches(|ptr, len| unsafe {
1351 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
1352 })
1353 }
1354
1355 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
1365 let mut change = ffi::TwigChange {
1366 old_span: ffi::TwigSpan { start: 0, end: 0 },
1367 new_span: ffi::TwigSpan { start: 0, end: 0 },
1368 };
1369 let status = unsafe {
1370 ffi::twig_editor_edit_range(
1371 self.raw.as_ptr(),
1372 start,
1373 end,
1374 text.as_ptr(),
1375 text.len(),
1376 &mut change,
1377 )
1378 };
1379 Error::from_status(status)?;
1380 Ok(Change::from_ffi(change))
1381 }
1382
1383 pub fn last_change(&mut self) -> Option<Change> {
1389 let mut change = ffi::TwigChange {
1390 old_span: ffi::TwigSpan { start: 0, end: 0 },
1391 new_span: ffi::TwigSpan { start: 0, end: 0 },
1392 };
1393 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
1394 match status.0 {
1395 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
1396 _ => None,
1397 }
1398 }
1399
1400 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
1405 let mut change = ffi::TwigChange {
1406 old_span: ffi::TwigSpan { start: 0, end: 0 },
1407 new_span: ffi::TwigSpan { start: 0, end: 0 },
1408 };
1409 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
1410 if status.0 == ffi::TwigStatus::NOT_FOUND {
1411 return Ok(None);
1412 }
1413 Error::from_status(status)?;
1414 Ok(Some(Change::from_ffi(change)))
1415 }
1416
1417 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
1421 let mut change = ffi::TwigChange {
1422 old_span: ffi::TwigSpan { start: 0, end: 0 },
1423 new_span: ffi::TwigSpan { start: 0, end: 0 },
1424 };
1425 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
1426 if status.0 == ffi::TwigStatus::NOT_FOUND {
1427 return Ok(None);
1428 }
1429 Error::from_status(status)?;
1430 Ok(Some(Change::from_ffi(change)))
1431 }
1432
1433 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
1438 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
1439 Error::from_status(status)
1440 }
1441
1442 pub fn revision(&mut self) -> u64 {
1448 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
1449 }
1450
1451 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
1472 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1473 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
1474 match status.0 {
1475 ffi::TwigStatus::OK => Some(span.start..span.end),
1476 _ => None,
1477 }
1478 }
1479
1480 pub fn clear_dirty(&mut self) {
1485 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
1486 }
1487
1488 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
1496 let status = unsafe {
1497 ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len())
1498 };
1499 Error::from_status(status)
1500 }
1501
1502 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
1507 let raw = self.raw.as_ptr();
1508 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
1509 }
1510
1511 pub fn document(&mut self) -> Result<DocumentView<'_>, Error> {
1520 let mut raw = std::ptr::null_mut();
1521 let status = unsafe { ffi::twig_editor_document(self.raw.as_ptr(), &mut raw) };
1522 Error::from_status(status)?;
1523 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1524 Ok(DocumentView {
1525 doc: Document { raw },
1526 _editor: PhantomData,
1527 })
1528 }
1529
1530 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1535 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1536 let mut len = 0usize;
1537 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
1538 Error::from_status(status)?;
1539 if len == 0 {
1540 return Ok(Vec::new());
1541 }
1542 if ptr.is_null() {
1543 return Err(Error::Internal);
1544 }
1545 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1546 raw.iter().map(flat_node_from_ffi).collect()
1547 }
1548
1549 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1556 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1557 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1558 let mut len = 0usize;
1559 let status =
1560 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
1561 Error::from_status(status)?;
1562 if len == 0 || ptr.is_null() {
1563 return Ok(Vec::new());
1564 }
1565 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1566 raw.iter().map(query_match_from_ffi).collect()
1567 }
1568
1569 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1577 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1578 let mut len = 0usize;
1579 let status =
1580 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
1581 Error::from_status(status)?;
1582 if len == 0 || ptr.is_null() {
1583 return Ok(Vec::new());
1584 }
1585 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1586 raw.iter().map(flat_node_from_ffi).collect()
1587 }
1588
1589 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1594 let mut m = ffi::TwigQueryMatch {
1595 node_id: 0,
1596 span: ffi::TwigSpan { start: 0, end: 0 },
1597 content_span: ffi::TwigSpan { start: 0, end: 0 },
1598 has_content_span: 0,
1599 kind: std::ptr::null(),
1600 };
1601 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
1602 match status.0 {
1603 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1604 ffi::TwigStatus::NOT_FOUND => Ok(None),
1605 _ => Err(Error::from_status(status).unwrap_err()),
1606 }
1607 }
1608
1609 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1613 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1614 let mut len = 0usize;
1615 let status =
1616 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
1617 match status.0 {
1618 ffi::TwigStatus::OK => {}
1619 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1620 _ => return Err(Error::from_status(status).unwrap_err()),
1621 }
1622 if len == 0 || ptr.is_null() {
1623 return Ok(Vec::new());
1624 }
1625 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1626 raw.iter().map(query_match_from_ffi).collect()
1627 }
1628
1629 pub fn wrap_range(
1637 &mut self,
1638 start: usize,
1639 end: usize,
1640 kind: InlineKind,
1641 ) -> Result<Change, Error> {
1642 self.change_op(|ed, out| unsafe {
1643 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
1644 })
1645 }
1646
1647 pub fn toggle_inline(
1652 &mut self,
1653 start: usize,
1654 end: usize,
1655 kind: InlineKind,
1656 ) -> Result<Change, Error> {
1657 self.change_op(|ed, out| unsafe {
1658 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
1659 })
1660 }
1661
1662 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
1681 let (block_kind, level) = kind.to_c();
1682 self.change_op(|ed, out| unsafe {
1683 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
1684 })
1685 }
1686
1687 pub fn toggle_block_container(
1710 &mut self,
1711 start: usize,
1712 end: usize,
1713 kind: BlockContainerKind,
1714 ) -> Result<Change, Error> {
1715 self.change_op(|ed, out| unsafe {
1716 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
1717 })
1718 }
1719
1720 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
1739 self.change_op(|ed, out| unsafe {
1740 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
1741 })?;
1742 Ok(())
1743 }
1744
1745 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
1754 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
1755 }
1756
1757 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
1760 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
1761 }
1762
1763 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
1765 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
1766 }
1767
1768 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
1770 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
1771 }
1772
1773 pub fn table_set_alignment(
1775 &mut self,
1776 offset: usize,
1777 alignment: Alignment,
1778 ) -> Result<(), Error> {
1779 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
1780 }
1781
1782 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
1784 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
1785 }
1786
1787 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
1789 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
1790 }
1791
1792 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
1793 self.change_op(|ed, out| unsafe { ffi::twig_editor_table_edit(ed, offset, op, arg, out) })?;
1794 Ok(())
1795 }
1796
1797 pub fn insert_link(
1842 &mut self,
1843 start: usize,
1844 end: usize,
1845 destination: &str,
1846 ) -> Result<Change, Error> {
1847 self.change_op(|ed, out| unsafe {
1848 ffi::twig_editor_insert_link(
1849 ed,
1850 start,
1851 end,
1852 destination.as_ptr(),
1853 destination.len(),
1854 out,
1855 )
1856 })
1857 }
1858
1859 pub fn insert_image(
1880 &mut self,
1881 start: usize,
1882 end: usize,
1883 destination: &str,
1884 ) -> Result<Change, Error> {
1885 self.change_op(|ed, out| unsafe {
1886 ffi::twig_editor_insert_image(
1887 ed,
1888 start,
1889 end,
1890 destination.as_ptr(),
1891 destination.len(),
1892 out,
1893 )
1894 })
1895 }
1896
1897 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
1918 self.change_op(|ed, out| unsafe {
1919 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
1920 })
1921 }
1922
1923 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
1937 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
1938 }
1939
1940 pub fn insert_thematic_break(&mut self, offset: usize) -> Result<Change, Error> {
1959 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_thematic_break(ed, offset, out) })
1960 }
1961
1962 pub fn split_block(&mut self, offset: usize) -> Result<Change, Error> {
2010 self.change_op(|ed, out| unsafe { ffi::twig_editor_split_block(ed, offset, out) })
2011 }
2012
2013 pub fn toggle_code_block(
2045 &mut self,
2046 start: usize,
2047 end: usize,
2048 language: Option<&str>,
2049 ) -> Result<Change, Error> {
2050 let (ptr, len, has) = opt_str(language);
2051 self.change_op(|ed, out| unsafe {
2052 ffi::twig_editor_toggle_code_block(ed, start, end, ptr, len, has, out)
2053 })
2054 }
2055
2056 pub fn set_code_language(
2066 &mut self,
2067 offset: usize,
2068 language: Option<&str>,
2069 ) -> Result<Change, Error> {
2070 let (ptr, len, has) = opt_str(language);
2071 self.change_op(|ed, out| unsafe {
2072 ffi::twig_editor_set_code_language(ed, offset, ptr, len, has, out)
2073 })
2074 }
2075
2076 pub fn toggle_task_item(&mut self, offset: usize) -> Result<Change, Error> {
2087 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_item(ed, offset, out) })
2088 }
2089
2090 pub fn set_task_checked(&mut self, offset: usize, checked: bool) -> Result<(), Error> {
2104 self.change_op(|ed, out| unsafe {
2105 ffi::twig_editor_set_task_checked(ed, offset, checked as c_int, out)
2106 })?;
2107 Ok(())
2108 }
2109
2110 pub fn toggle_task_checked(&mut self, offset: usize) -> Result<Change, Error> {
2115 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_checked(ed, offset, out) })
2116 }
2117
2118 pub fn insert_footnote(&mut self, offset: usize, label: &str) -> Result<Change, Error> {
2137 self.change_op(|ed, out| unsafe {
2138 ffi::twig_editor_insert_footnote(ed, offset, label.as_ptr(), label.len(), out)
2139 })
2140 }
2141
2142 fn change_op(
2145 &mut self,
2146 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
2147 ) -> Result<Change, Error> {
2148 let mut change = ffi::TwigChange {
2149 old_span: ffi::TwigSpan { start: 0, end: 0 },
2150 new_span: ffi::TwigSpan { start: 0, end: 0 },
2151 };
2152 let status = op(self.raw.as_ptr(), &mut change);
2153 Error::from_status(status)?;
2154 Ok(Change::from_ffi(change))
2155 }
2156
2157 fn apply(
2159 &mut self,
2160 locator: &str,
2161 text: &str,
2162 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
2163 ) -> Result<(), Error> {
2164 let status = op(
2165 self.raw.as_ptr(),
2166 locator.as_ptr(),
2167 locator.len(),
2168 text.as_ptr(),
2169 text.len(),
2170 );
2171 Error::from_status(status)
2172 }
2173}
2174
2175impl Drop for Editor {
2176 fn drop(&mut self) {
2177 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
2178 }
2179}
2180
2181fn collect_bytes(
2186 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
2187) -> Result<Vec<u8>, Error> {
2188 let mut ptr = std::ptr::null();
2189 let mut len = 0usize;
2190 let status = call(&mut ptr, &mut len);
2191 Error::from_status(status)?;
2192 if len == 0 {
2193 return Ok(Vec::new());
2194 }
2195 if ptr.is_null() {
2196 return Err(Error::Internal);
2197 }
2198 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2199 Ok(bytes.to_vec())
2200}
2201
2202fn collect_matches(
2205 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
2206) -> Result<Vec<QueryMatch>, Error> {
2207 let mut ptr = std::ptr::null();
2208 let mut len = 0usize;
2209 let status = call(&mut ptr, &mut len);
2210 Error::from_status(status)?;
2211 if len == 0 {
2212 return Ok(Vec::new());
2213 }
2214 if ptr.is_null() {
2215 return Err(Error::Internal);
2216 }
2217 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
2218 matches.iter().map(query_match_from_ffi).collect()
2219}
2220
2221fn collect_flat_nodes(
2224 call: impl FnOnce(*mut *const ffi::TwigFlatNode, *mut usize) -> ffi::TwigStatus,
2225) -> Result<Vec<FlatNode>, Error> {
2226 let mut ptr = std::ptr::null();
2227 let mut len = 0usize;
2228 let status = call(&mut ptr, &mut len);
2229 Error::from_status(status)?;
2230 if len == 0 {
2231 return Ok(Vec::new());
2232 }
2233 if ptr.is_null() {
2234 return Err(Error::Internal);
2235 }
2236 let nodes = unsafe { std::slice::from_raw_parts(ptr, len) };
2237 nodes.iter().map(flat_node_from_ffi).collect()
2238}
2239
2240fn empty_ffi_match() -> ffi::TwigQueryMatch {
2242 ffi::TwigQueryMatch {
2243 node_id: 0,
2244 span: ffi::TwigSpan { start: 0, end: 0 },
2245 content_span: ffi::TwigSpan { start: 0, end: 0 },
2246 has_content_span: 0,
2247 kind: std::ptr::null(),
2248 }
2249}
2250
2251fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
2254 Ok(QueryMatch {
2255 node_id: m.node_id,
2256 span: m.span.start..m.span.end,
2257 content_span: if m.has_content_span != 0 {
2258 Some(m.content_span.start..m.content_span.end)
2259 } else {
2260 None
2261 },
2262 kind: Kind::from(borrowed_cstr(m.kind)?.as_str()),
2263 })
2264}
2265
2266fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
2268 let node_id = |v: u32| {
2269 if v == ffi::TWIG_NO_NODE {
2270 None
2271 } else {
2272 Some(NodeId(v))
2273 }
2274 };
2275 Ok(FlatNode {
2276 id: NodeId(n.id),
2277 parent: node_id(n.parent),
2278 first_child: node_id(n.first_child),
2279 next_sibling: node_id(n.next_sibling),
2280 span: n.span.start..n.span.end,
2281 content_span: if n.has_content_span != 0 {
2282 Some(n.content_span.start..n.content_span.end)
2283 } else {
2284 None
2285 },
2286 level: if n.level != 0 { Some(n.level) } else { None },
2287 kind: Kind::from(borrowed_cstr(n.kind)?.as_str()),
2288 text: borrowed_bytes(n.text_ptr, n.text_len),
2289 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
2290 head: match n.head {
2291 ffi::TWIG_HEAD_NONE => None,
2292 v => Some(v != 0),
2293 },
2294 alignment: Alignment::from_c(n.alignment),
2295 name: borrowed_bytes(n.name_ptr, n.name_len),
2296 directive_form: DirectiveForm::from_c(n.directive_form),
2297 origin: ContainerOrigin::from_c(n.container_origin),
2298 marker_span: if n.has_marker_span != 0 {
2299 Some(n.marker_span.start..n.marker_span.end)
2300 } else {
2301 None
2302 },
2303 checked: match n.checked {
2304 ffi::TWIG_TASK_CHECKED_NONE => None,
2305 v => Some(v != 0),
2306 },
2307 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
2308 })
2309}
2310
2311fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
2315 if ptr.is_null() || len == 0 {
2316 return Vec::new();
2317 }
2318 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
2319 kvs.iter()
2320 .map(|kv| {
2321 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
2322 (key, borrowed_bytes(kv.value, kv.value_len))
2323 })
2324 .collect()
2325}
2326
2327fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
2329 if ptr.is_null() {
2330 return Err(Error::Internal);
2331 }
2332 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
2333 .to_str()
2334 .map_err(|_| Error::Internal)?
2335 .to_owned())
2336}
2337
2338fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
2342 if ptr.is_null() {
2343 return None;
2344 }
2345 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2346 Some(String::from_utf8_lossy(bytes).into_owned())
2347}
2348
2349#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
2353pub struct NodeId(pub u32);
2354
2355#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2358pub enum VoidKind {
2359 Doc,
2360 Para,
2361 ThematicBreak,
2362 Section,
2363 Div,
2364 BlockQuote,
2365 DefinitionList,
2366 Table,
2367 ListItem,
2368 DefinitionListItem,
2369 Term,
2370 Definition,
2371 Caption,
2372 SoftBreak,
2373 HardBreak,
2374 NonBreakingSpace,
2375 Emph,
2376 Strong,
2377 Span,
2378 Mark,
2379 Superscript,
2380 Subscript,
2381 Insert,
2382 Delete,
2383 DoubleQuoted,
2384 SingleQuoted,
2385}
2386
2387impl VoidKind {
2388 fn to_c(self) -> c_int {
2389 match self {
2391 VoidKind::Doc => 0,
2392 VoidKind::Para => 1,
2393 VoidKind::ThematicBreak => 3,
2394 VoidKind::Section => 4,
2395 VoidKind::Div => 5,
2396 VoidKind::BlockQuote => 9,
2397 VoidKind::DefinitionList => 13,
2398 VoidKind::Table => 14,
2399 VoidKind::ListItem => 15,
2400 VoidKind::DefinitionListItem => 17,
2401 VoidKind::Term => 18,
2402 VoidKind::Definition => 19,
2403 VoidKind::Caption => 22,
2404 VoidKind::SoftBreak => 26,
2405 VoidKind::HardBreak => 27,
2406 VoidKind::NonBreakingSpace => 28,
2407 VoidKind::Emph => 38,
2408 VoidKind::Strong => 39,
2409 VoidKind::Span => 42,
2410 VoidKind::Mark => 43,
2411 VoidKind::Superscript => 44,
2412 VoidKind::Subscript => 45,
2413 VoidKind::Insert => 46,
2414 VoidKind::Delete => 47,
2415 VoidKind::DoubleQuoted => 48,
2416 VoidKind::SingleQuoted => 49,
2417 }
2418 }
2419}
2420
2421#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2423pub enum TextKind {
2424 Str,
2425 Symb,
2426 Verbatim,
2427 InlineMath,
2428 DisplayMath,
2429 Url,
2430 Email,
2431 FootnoteReference,
2432 CitationReference,
2435 SubstitutionReference,
2437 Comment,
2438 Doctype,
2439 Cdata,
2440}
2441
2442impl TextKind {
2443 fn to_c(self) -> c_int {
2444 match self {
2445 TextKind::Str => 25,
2446 TextKind::Symb => 29,
2447 TextKind::Verbatim => 30,
2448 TextKind::InlineMath => 32,
2449 TextKind::DisplayMath => 33,
2450 TextKind::Url => 34,
2451 TextKind::Email => 35,
2452 TextKind::FootnoteReference => 36,
2453 TextKind::CitationReference => 58,
2454 TextKind::SubstitutionReference => 59,
2455 TextKind::Comment => 52,
2456 TextKind::Doctype => 53,
2457 TextKind::Cdata => 55,
2458 }
2459 }
2460}
2461
2462#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2464pub enum BulletStyle {
2465 Dash,
2466 Plus,
2467 Star,
2468}
2469
2470impl BulletStyle {
2471 fn to_c(self) -> c_int {
2472 match self {
2473 BulletStyle::Dash => 0,
2474 BulletStyle::Plus => 1,
2475 BulletStyle::Star => 2,
2476 }
2477 }
2478}
2479
2480#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2482pub enum OrderedNumbering {
2483 Decimal,
2484 LowerAlpha,
2485 UpperAlpha,
2486 LowerRoman,
2487 UpperRoman,
2488}
2489
2490impl OrderedNumbering {
2491 fn to_c(self) -> c_int {
2492 match self {
2493 OrderedNumbering::Decimal => 0,
2494 OrderedNumbering::LowerAlpha => 1,
2495 OrderedNumbering::UpperAlpha => 2,
2496 OrderedNumbering::LowerRoman => 3,
2497 OrderedNumbering::UpperRoman => 4,
2498 }
2499 }
2500}
2501
2502#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2504pub enum OrderedDelim {
2505 Period,
2506 ParenAfter,
2507 ParenBoth,
2508}
2509
2510impl OrderedDelim {
2511 fn to_c(self) -> c_int {
2512 match self {
2513 OrderedDelim::Period => 0,
2514 OrderedDelim::ParenAfter => 1,
2515 OrderedDelim::ParenBoth => 2,
2516 }
2517 }
2518}
2519
2520#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2523pub enum Alignment {
2524 Default,
2525 Left,
2526 Right,
2527 Center,
2528}
2529
2530impl Alignment {
2531 fn to_c(self) -> c_int {
2532 match self {
2533 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
2534 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
2535 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
2536 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
2537 }
2538 }
2539
2540 fn from_c(v: c_int) -> Option<Self> {
2543 match v {
2544 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
2545 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
2546 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
2547 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
2548 _ => None,
2549 }
2550 }
2551}
2552
2553#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2555pub enum SmartPunctuation {
2556 LeftSingleQuote,
2557 RightSingleQuote,
2558 LeftDoubleQuote,
2559 RightDoubleQuote,
2560 Ellipses,
2561 EmDash,
2562 EnDash,
2563}
2564
2565impl SmartPunctuation {
2566 fn to_c(self) -> c_int {
2567 match self {
2568 SmartPunctuation::LeftSingleQuote => 0,
2569 SmartPunctuation::RightSingleQuote => 1,
2570 SmartPunctuation::LeftDoubleQuote => 2,
2571 SmartPunctuation::RightDoubleQuote => 3,
2572 SmartPunctuation::Ellipses => 4,
2573 SmartPunctuation::EmDash => 5,
2574 SmartPunctuation::EnDash => 6,
2575 }
2576 }
2577}
2578
2579#[derive(Clone, Debug, Eq, PartialEq)]
2594pub struct Warning {
2595 pub fidelity: Fidelity,
2596 pub path: String,
2603 pub kind: Kind,
2606}
2607
2608#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2610#[non_exhaustive]
2611pub enum Fidelity {
2612 Degraded,
2615 Dropped,
2617}
2618
2619impl Fidelity {
2620 fn from_c(v: c_int) -> Self {
2624 match v {
2625 ffi::TWIG_FIDELITY_DROPPED => Fidelity::Dropped,
2626 _ => Fidelity::Degraded,
2627 }
2628 }
2629}
2630
2631#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2632#[non_exhaustive]
2633pub enum ContainerOrigin {
2634 Element,
2636 Directive,
2640}
2641
2642impl ContainerOrigin {
2643 fn from_c(v: c_int) -> Option<Self> {
2646 match v {
2647 ffi::TWIG_CONTAINER_ORIGIN_ELEMENT => Some(ContainerOrigin::Element),
2648 ffi::TWIG_CONTAINER_ORIGIN_DIRECTIVE => Some(ContainerOrigin::Directive),
2649 _ => None,
2650 }
2651 }
2652}
2653
2654#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2656pub enum DirectiveForm {
2657 Text,
2658 Leaf,
2659 Container,
2660}
2661
2662impl DirectiveForm {
2663 fn to_c(self) -> c_int {
2664 match self {
2665 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
2666 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
2667 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
2668 }
2669 }
2670
2671 fn from_c(v: c_int) -> Option<Self> {
2675 match v {
2676 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
2677 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
2678 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
2679 _ => None,
2680 }
2681 }
2682}
2683
2684fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
2688 match s {
2689 Some(x) => (x.as_ptr(), x.len(), 1),
2690 None => (std::ptr::null(), 0, 0),
2691 }
2692}
2693
2694#[derive(Debug)]
2701pub struct Builder {
2702 raw: NonNull<ffi::TwigBuilder>,
2703}
2704
2705impl Builder {
2706 pub fn new() -> Result<Self, Error> {
2708 let mut raw = std::ptr::null_mut();
2709 let status = unsafe { ffi::twig_builder_create(&mut raw) };
2710 Error::from_status(status)?;
2711 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
2712 Ok(Self { raw })
2713 }
2714
2715 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
2718 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
2719 }
2720
2721 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
2723 self.emit(|b, out| unsafe {
2724 ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out)
2725 })
2726 }
2727
2728 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
2730 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
2731 }
2732
2733 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
2735 let (lp, ll, has) = opt_str(lang);
2736 self.emit(|b, out| unsafe {
2737 ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out)
2738 })
2739 }
2740
2741 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
2743 self.emit(|b, out| unsafe {
2744 ffi::twig_builder_add_raw_block(
2745 b,
2746 format.as_ptr(),
2747 format.len(),
2748 text.as_ptr(),
2749 text.len(),
2750 out,
2751 )
2752 })
2753 }
2754
2755 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
2757 self.emit(|b, out| unsafe {
2758 ffi::twig_builder_add_metadata(
2759 b,
2760 lang.as_ptr(),
2761 lang.len(),
2762 text.as_ptr(),
2763 text.len(),
2764 out,
2765 )
2766 })
2767 }
2768
2769 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
2771 self.emit(|b, out| unsafe {
2772 ffi::twig_builder_add_raw_inline(
2773 b,
2774 format.as_ptr(),
2775 format.len(),
2776 text.as_ptr(),
2777 text.len(),
2778 out,
2779 )
2780 })
2781 }
2782
2783 pub fn add_smart_punctuation(
2788 &mut self,
2789 kind: SmartPunctuation,
2790 text: &str,
2791 ) -> Result<NodeId, Error> {
2792 self.emit(|b, out| unsafe {
2793 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
2794 })
2795 }
2796
2797 pub fn add_link(
2800 &mut self,
2801 destination: Option<&str>,
2802 reference: Option<&str>,
2803 ) -> Result<NodeId, Error> {
2804 let (dp, dl, hd) = opt_str(destination);
2805 let (rp, rl, hr) = opt_str(reference);
2806 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
2807 }
2808
2809 pub fn add_image(
2811 &mut self,
2812 destination: Option<&str>,
2813 reference: Option<&str>,
2814 ) -> Result<NodeId, Error> {
2815 let (dp, dl, hd) = opt_str(destination);
2816 let (rp, rl, hr) = opt_str(reference);
2817 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
2818 }
2819
2820 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
2822 self.emit(|b, out| unsafe {
2823 ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out)
2824 })
2825 }
2826
2827 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
2829 self.emit(|b, out| unsafe {
2830 ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out)
2831 })
2832 }
2833
2834 pub fn add_processing_instruction(
2836 &mut self,
2837 target: &str,
2838 data: &str,
2839 ) -> Result<NodeId, Error> {
2840 self.emit(|b, out| unsafe {
2841 ffi::twig_builder_add_processing_instruction(
2842 b,
2843 target.as_ptr(),
2844 target.len(),
2845 data.as_ptr(),
2846 data.len(),
2847 out,
2848 )
2849 })
2850 }
2851
2852 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
2854 self.emit(|b, out| unsafe {
2855 ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out)
2856 })
2857 }
2858
2859 pub fn add_citation(&mut self, label: &str) -> Result<NodeId, Error> {
2864 self.emit(|b, out| unsafe {
2865 ffi::twig_builder_add_citation(b, label.as_ptr(), label.len(), out)
2866 })
2867 }
2868
2869 pub fn add_substitution(&mut self, label: &str) -> Result<NodeId, Error> {
2873 self.emit(|b, out| unsafe {
2874 ffi::twig_builder_add_substitution(b, label.as_ptr(), label.len(), out)
2875 })
2876 }
2877
2878 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
2880 self.emit(|b, out| unsafe {
2881 ffi::twig_builder_add_reference(
2882 b,
2883 label.as_ptr(),
2884 label.len(),
2885 destination.as_ptr(),
2886 destination.len(),
2887 out,
2888 )
2889 })
2890 }
2891
2892 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
2894 self.emit(|b, out| unsafe {
2895 ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out)
2896 })
2897 }
2898
2899 pub fn add_ordered_list(
2901 &mut self,
2902 numbering: OrderedNumbering,
2903 delim: OrderedDelim,
2904 tight: bool,
2905 start: Option<u32>,
2906 ) -> Result<NodeId, Error> {
2907 let (start_val, has_start) = match start {
2908 Some(s) => (s, 1),
2909 None => (0, 0),
2910 };
2911 self.emit(|b, out| unsafe {
2912 ffi::twig_builder_add_ordered_list(
2913 b,
2914 numbering.to_c(),
2915 delim.to_c(),
2916 tight as c_int,
2917 start_val,
2918 has_start,
2919 out,
2920 )
2921 })
2922 }
2923
2924 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
2926 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
2927 }
2928
2929 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
2931 self.emit(|b, out| unsafe {
2932 ffi::twig_builder_add_task_list_item(b, checked as c_int, out)
2933 })
2934 }
2935
2936 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
2938 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
2939 }
2940
2941 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
2943 self.emit(|b, out| unsafe {
2944 ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out)
2945 })
2946 }
2947
2948 pub fn add_cell_spanning(
2953 &mut self,
2954 head: bool,
2955 alignment: Alignment,
2956 colspan: u32,
2957 rowspan: u32,
2958 ) -> Result<NodeId, Error> {
2959 self.emit(|b, out| unsafe {
2960 ffi::twig_builder_add_cell_spanning(
2961 b,
2962 head as c_int,
2963 alignment.to_c(),
2964 colspan,
2965 rowspan,
2966 out,
2967 )
2968 })
2969 }
2970
2971 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
2974 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
2975 let status = unsafe {
2976 ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len())
2977 };
2978 Error::from_status(status)
2979 }
2980
2981 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
2985 let kvs: Vec<ffi::TwigKeyVal> = attrs
2986 .iter()
2987 .map(|(k, v)| ffi::TwigKeyVal {
2988 key: k.as_ptr(),
2989 key_len: k.len(),
2990 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
2991 value_len: v.map_or(0, |s| s.len()),
2992 })
2993 .collect();
2994 let status = unsafe {
2995 ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len())
2996 };
2997 Error::from_status(status)
2998 }
2999
3000 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3003 let raw = self.raw.as_ptr();
3004 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
3005 }
3006
3007 pub fn serialize_to(&mut self, root: NodeId, target: Target) -> Result<Vec<u8>, Error> {
3014 let raw = self.raw.as_ptr();
3015 let ffi_target: ffi::TwigFormat = target.into();
3016 collect_bytes(|ptr, len| unsafe {
3017 ffi::twig_builder_serialize(raw, root.0, ffi_target as i32, ptr, len)
3018 })
3019 }
3020
3021 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
3026 self.serialize_to(root, format.into())
3027 }
3028
3029 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3031 let raw = self.raw.as_ptr();
3032 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
3033 }
3034
3035 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
3038 let raw = self.raw.as_ptr();
3039 collect_matches(|ptr, len| unsafe {
3040 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
3041 })
3042 }
3043
3044 fn emit(
3047 &mut self,
3048 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
3049 ) -> Result<NodeId, Error> {
3050 let mut id: u32 = 0;
3051 let status = call(self.raw.as_ptr(), &mut id);
3052 Error::from_status(status)?;
3053 Ok(NodeId(id))
3054 }
3055}
3056
3057impl Drop for Builder {
3058 fn drop(&mut self) {
3059 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
3060 }
3061}
3062
3063#[cfg(test)]
3064mod tests {
3065 use super::*;
3066
3067 #[test]
3068 fn abi_version_matches() {
3069 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
3073 }
3074
3075 #[test]
3076 fn parses_and_renders_markdown_html() {
3077 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3078 let html = doc.render_html().expect("render html");
3079 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
3080 }
3081
3082 #[test]
3083 fn parses_html_input() {
3084 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
3085 let html = doc.render_html().expect("render html");
3086 assert!(String::from_utf8_lossy(&html).contains("hi"));
3087 }
3088
3089 #[test]
3090 fn parses_asciidoc_and_refuses_to_write_it() {
3091 let mut doc = Document::parse_str("= Title\n\nsome *bold* text\n", Format::Asciidoc)
3092 .expect("parse asciidoc");
3093 let html = String::from_utf8_lossy(&doc.render_html().expect("render html")).into_owned();
3094 assert!(html.contains("<h1>Title</h1>"), "got {html:?}");
3095 assert!(html.contains("<strong>bold</strong>"), "got {html:?}");
3096
3097 assert_eq!(
3101 doc.serialize_to(Target::Asciidoc),
3102 Err(Error::UnsupportedFormat)
3103 );
3104 assert_eq!(Target::from(Format::Asciidoc), Target::Asciidoc);
3105 assert_eq!(Target::Asciidoc.as_format(), Some(Format::Asciidoc));
3106 }
3107
3108 #[test]
3109 fn serialize_round_trips_and_cross_converts() {
3110 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3111
3112 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
3113 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
3114
3115 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
3117 }
3118
3119 #[test]
3120 fn serialize_markdown_to_djot() {
3121 let mut doc =
3122 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3123 let djot = doc.serialize(Format::Djot).expect("serialize djot");
3124 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3125 }
3126
3127 #[test]
3128 fn serialize_to_takes_the_output_axis() {
3129 let mut doc =
3130 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3131
3132 let djot = doc.serialize_to(Target::Djot).expect("serialize djot");
3133 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3134
3135 assert_eq!(doc.serialize_to(Target::Xml), Err(Error::UnsupportedFormat));
3138 }
3139
3140 #[test]
3141 fn serialize_and_serialize_to_agree() {
3142 let mut a = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3145 let mut b = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3146 for format in [Format::Markdown, Format::Djot, Format::Html] {
3147 assert_eq!(a.serialize(format), b.serialize_to(Target::from(format)));
3148 }
3149 }
3150
3151 #[test]
3152 fn every_format_is_a_target_that_names_it_back() {
3153 for format in [Format::Djot, Format::Markdown, Format::Xml, Format::Html] {
3156 assert_eq!(Target::from(format).as_format(), Some(format));
3157 }
3158 }
3159
3160 #[test]
3161 fn ast_json_dumps_the_tree() {
3162 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
3163 let json = doc.ast_json().expect("ast json");
3164 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
3165 }
3166
3167 #[test]
3168 fn query_finds_nodes_by_selector() {
3169 let source = "# One\n\n## Two\n";
3170 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3171 let matches = doc.query("heading").expect("query");
3172
3173 assert_eq!(matches.len(), 2);
3174 for m in &matches {
3175 assert_eq!(m.kind, Kind::Heading);
3176 assert!(m.span.start < m.span.end);
3177 }
3178 }
3179
3180 #[test]
3181 fn query_recovers_code_spans() {
3182 let source = "prose `code` more prose\n";
3183 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3184 let matches = doc.query("verbatim").expect("query");
3185
3186 assert_eq!(matches.len(), 1);
3187 assert_eq!(&source[matches[0].span.clone()], "`code`");
3188 }
3189
3190 #[test]
3191 fn document_span_accessors_read_by_node_id() {
3192 let source = "# hi\n\ntext\n";
3193 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3194 let heading = doc.query("heading").expect("query").pop().expect("heading");
3195
3196 assert_eq!(
3197 doc.span(NodeId(heading.node_id)).expect("span"),
3198 heading.span
3199 );
3200 assert_eq!(
3201 doc.content_span(NodeId(heading.node_id))
3202 .expect("content span"),
3203 heading.content_span
3204 );
3205 assert_eq!(doc.span(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3206 }
3207
3208 #[test]
3209 fn document_walks_its_tree_without_an_editor() {
3210 let source = "# hi\n\ntext\n";
3211 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3212
3213 let nodes = doc.nodes().expect("nodes");
3214 assert!(nodes.len() >= 3);
3215 for (i, n) in nodes.iter().enumerate() {
3216 assert_eq!(n.id, NodeId(i as u32));
3217 }
3218
3219 let kids = doc.children(None).expect("children");
3220 assert_eq!(kids.len(), 2);
3221 assert_eq!(kids[0].kind, Kind::Heading);
3222
3223 let sub = doc.subtree(NodeId(kids[0].node_id)).expect("subtree");
3224 assert_eq!(sub[0].id, NodeId(0));
3225 assert_eq!(sub[0].parent, None);
3226 assert_eq!(sub[0].span, kids[0].span);
3227
3228 let hit = doc.node_at(2).expect("node_at").expect("a node at 2");
3229 let chain = doc.ancestors_at(2).expect("ancestors");
3230 assert_eq!(chain.last().expect("deepest").node_id, hit.node_id);
3231 assert_eq!(chain[0].kind, Kind::Doc);
3232
3233 assert_eq!(doc.subtree(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3234 }
3235
3236 #[test]
3237 fn editor_document_view_reads_the_live_tree() {
3238 let mut ed = Editor::new_str("# one\n\ntwo\n", Format::Markdown).expect("editor");
3239
3240 {
3241 let mut view = ed.document().expect("view");
3242 let kids = view.children(None).expect("children");
3243 assert_eq!(kids.len(), 2);
3244 assert_eq!(kids[0].kind, Kind::Heading);
3245 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..5);
3246 assert_eq!(view.render_html(), Err(Error::UnsupportedFormat));
3248 assert_eq!(
3249 view.serialize(Format::Markdown),
3250 Err(Error::UnsupportedFormat)
3251 );
3252 }
3253
3254 ed.replace("0", "# one and a half").expect("replace");
3255 let mut view = ed.document().expect("view");
3256 let kids = view.children(None).expect("children");
3257 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..16);
3258 }
3259
3260 #[test]
3261 fn query_rejects_a_malformed_selector() {
3262 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
3263 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
3264 }
3265
3266 #[test]
3267 fn editor_edits_by_index_path() {
3268 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
3269 ed.replace_content("0.0", "bye").expect("replace_content");
3270 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
3271 }
3272
3273 #[test]
3274 fn flat_nodes_expose_element_name_and_attrs() {
3275 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
3279 let mut ed = Editor::new_ext(
3280 src.as_bytes(),
3281 Format::Markdown,
3282 MarkdownExtensions {
3283 html_elements: true,
3284 ..Default::default()
3285 },
3286 )
3287 .expect("editor");
3288 let nodes = ed.nodes().expect("nodes");
3289
3290 let source = nodes
3291 .iter()
3292 .find(|n| n.name.as_deref() == Some("source"))
3293 .expect("a <source> element node");
3294 assert_eq!(
3295 source.attrs,
3296 vec![
3297 (
3298 "media".to_string(),
3299 Some("(prefers-color-scheme: dark)".to_string())
3300 ),
3301 ("srcset".to_string(), Some("d.svg".to_string())),
3302 ]
3303 );
3304
3305 let img = nodes
3308 .iter()
3309 .find(|n| n.kind == Kind::Image)
3310 .expect("an image node");
3311 assert!(img.name.is_none());
3312 assert_eq!(img.destination.as_deref(), Some("l.svg"));
3313
3314 let picture_kids_str = nodes.iter().find(|n| n.kind == Kind::Str);
3316 if let Some(s) = picture_kids_str {
3317 assert!(s.name.is_none() && s.attrs.is_empty());
3318 }
3319 }
3320
3321 #[test]
3322 fn definitions_finds_what_a_walk_from_the_root_cannot() {
3323 let mut doc = Document::parse_str(
3327 "text[^1] [x][a]\n\n[^1]: note\n\n[a]: /u\n",
3328 Format::Markdown,
3329 )
3330 .expect("parse markdown");
3331
3332 let defs = doc.definitions().expect("definitions");
3333 let mut kinds: Vec<Kind> = defs.iter().map(|m| m.kind.clone()).collect();
3334 kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3335 assert_eq!(kinds, vec![Kind::Footnote, Kind::Reference]);
3336
3337 let all = doc.nodes().expect("nodes");
3340 let root = all
3341 .iter()
3342 .find(|n| n.kind == Kind::Doc)
3343 .expect("a doc root");
3344 let mut reachable = vec![root.id];
3345 let mut i = 0;
3346 while i < reachable.len() {
3347 let n = &all[reachable[i].0 as usize];
3348 let mut c = n.first_child;
3349 while let Some(cid) = c {
3350 reachable.push(cid);
3351 c = all[cid.0 as usize].next_sibling;
3352 }
3353 i += 1;
3354 }
3355 for d in &defs {
3356 assert!(
3357 !reachable.contains(&NodeId(d.node_id)),
3358 "{} should be unreachable from the root",
3359 d.kind
3360 );
3361 }
3362
3363 let mut plain = Document::parse_str("just text\n", Format::Markdown).expect("parse");
3365 assert_eq!(plain.definitions().expect("definitions"), Vec::new());
3366 }
3367
3368 #[test]
3369 fn kind_round_trips_through_its_published_name() {
3370 for k in [
3374 Kind::Doc,
3375 Kind::Para,
3376 Kind::Heading,
3377 Kind::Container,
3378 Kind::TaskListItem,
3379 Kind::Superscript,
3380 Kind::FootnoteReference,
3381 Kind::ProcessingInstruction,
3382 Kind::Cdata,
3383 ] {
3384 assert_eq!(Kind::from(k.as_str()), k, "{k} did not round-trip");
3385 assert!(!k.is_unknown());
3386 }
3387 }
3388
3389 #[test]
3390 fn an_unknown_kind_name_is_carried_rather_than_lost() {
3391 let k = Kind::from("some_future_kind");
3394 assert!(k.is_unknown());
3395 assert_eq!(k.as_str(), "some_future_kind");
3396 assert_eq!(k, Kind::Other("some_future_kind".to_string()));
3397 }
3398
3399 #[test]
3400 fn every_kind_the_library_publishes_has_a_variant() {
3401 let cases: &[(&str, Format, MarkdownExtensions)] = &[
3406 (
3407 "# h\n\npara *emph* **strong** `code`\n\n- a\n- b\n\n1. c\n\n> q\n\n---\n\n```zig\nx\n```\n",
3408 Format::Markdown,
3409 MarkdownExtensions {
3410 directives: false,
3411 math: false,
3412 html_elements: false,
3413 },
3414 ),
3415 (
3416 "| a | b |\n| --- | --- |\n| 1 | 2 |\n\n- [ ] task\n- [x] done\n\nfoot[^1]\n\n[^1]: note\n\n[l]: /u\n\n[x][l]\n",
3417 Format::Markdown,
3418 MarkdownExtensions::default(),
3419 ),
3420 (
3421 ":::note\nbody\n:::\n\n:role[x]\n\n$a+b$\n",
3422 Format::Markdown,
3423 MarkdownExtensions {
3424 directives: true,
3425 math: true,
3426 html_elements: false,
3427 },
3428 ),
3429 (
3430 "a^b^ c~d~ {=e=} {+f+} {-g-} 'q' \"dq\"\n\n\n\n<https://e.com>\n",
3431 Format::Djot,
3432 MarkdownExtensions::default(),
3433 ),
3434 (
3435 "<!-- c --><!DOCTYPE html><video controls><p>x</p></video>",
3436 Format::Html,
3437 MarkdownExtensions::default(),
3438 ),
3439 ];
3440
3441 let mut unknown: Vec<String> = Vec::new();
3442 let mut seen: Vec<String> = Vec::new();
3443 for (src, format, ext) in cases {
3444 let mut ed = Editor::new_ext(src.as_bytes(), *format, *ext).expect("editor");
3445 for n in ed.nodes().expect("nodes") {
3446 if n.kind.is_unknown() {
3447 unknown.push(n.kind.as_str().to_string());
3448 }
3449 seen.push(n.kind.as_str().to_string());
3450 }
3451 }
3452 unknown.sort();
3453 unknown.dedup();
3454 assert!(unknown.is_empty(), "kinds with no variant: {unknown:?}");
3455
3456 seen.sort();
3459 seen.dedup();
3460 assert!(
3461 seen.len() >= 30,
3462 "only {} distinct kinds reached: {seen:?}",
3463 seen.len()
3464 );
3465 }
3466
3467 #[test]
3468 fn diagnostics_report_what_a_conversion_would_lose() {
3469 let mut doc = Document::parse_str("a^b^ c\n", Format::Djot).expect("parse djot");
3473
3474 let to_md = doc
3475 .diagnostics(Target::Markdown)
3476 .expect("markdown diagnostics");
3477 assert_eq!(
3478 to_md,
3479 vec![Warning {
3480 fidelity: Fidelity::Degraded,
3481 path: "0/1".to_string(),
3482 kind: Kind::Superscript,
3483 }]
3484 );
3485
3486 assert_eq!(
3488 doc.diagnostics(Target::Djot).expect("djot diagnostics"),
3489 Vec::new()
3490 );
3491 }
3492
3493 #[test]
3494 fn diagnostics_separate_a_droppable_node_from_a_degradable_one() {
3495 let mut doc =
3499 Document::parse_str("<p>hi</p><!-- secret -->", Format::Html).expect("parse html");
3500 let warnings = doc.diagnostics(Target::Djot).expect("djot diagnostics");
3501 let comment = warnings
3502 .iter()
3503 .find(|w| w.kind == Kind::Comment)
3504 .expect("a warning about the comment");
3505 assert_eq!(comment.fidelity, Fidelity::Dropped);
3506 }
3507
3508 #[test]
3509 fn diagnostics_refuse_a_target_with_no_serializer() {
3510 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3513 assert_eq!(doc.diagnostics(Target::Xml), Err(Error::UnsupportedFormat));
3514 assert_eq!(
3515 doc.diagnostics(Target::Asciidoc),
3516 Err(Error::UnsupportedFormat)
3517 );
3518 }
3519
3520 #[test]
3521 fn diagnostics_flag_a_header_less_table_and_leave_a_headed_one_alone() {
3522 let mut headed = Document::parse_str(
3527 "<table><tr><th>H</th></tr><tr><td>a</td></tr></table>",
3528 Format::Html,
3529 )
3530 .expect("parse headed table");
3531 assert!(
3532 headed
3533 .diagnostics(Target::Markdown)
3534 .expect("diagnostics")
3535 .iter()
3536 .all(|w| w.kind != Kind::Table)
3537 );
3538
3539 let mut headless = Document::parse_str("<table><tr><td>a</td></tr></table>", Format::Html)
3540 .expect("parse header-less table");
3541 let table_warning = headless
3542 .diagnostics(Target::Markdown)
3543 .expect("diagnostics")
3544 .into_iter()
3545 .find(|w| w.kind == Kind::Table)
3546 .expect("a warning about the table");
3547 assert_eq!(table_warning.fidelity, Fidelity::Degraded);
3548 }
3549
3550 #[test]
3551 fn container_origin_separates_a_div_from_a_div() {
3552 let mut html =
3557 Editor::new("<div>hi</div>\n".as_bytes(), Format::Html).expect("html editor");
3558 let mut md = Editor::new_ext(
3559 ":::div\nhi\n:::\n".as_bytes(),
3560 Format::Markdown,
3561 MarkdownExtensions {
3562 directives: true,
3563 ..Default::default()
3564 },
3565 )
3566 .expect("markdown editor");
3567
3568 let html_nodes = html.nodes().expect("html nodes");
3569 let md_nodes = md.nodes().expect("markdown nodes");
3570 let tag = html_nodes
3571 .iter()
3572 .find(|n| n.name.as_deref() == Some("div"))
3573 .expect("a <div> container");
3574 let directive = md_nodes
3575 .iter()
3576 .find(|n| n.name.as_deref() == Some("div"))
3577 .expect("a :::div container");
3578
3579 assert_eq!(tag.kind, directive.kind);
3581 assert_eq!(tag.name, directive.name);
3582 assert_eq!(tag.directive_form, directive.directive_form);
3583 assert_eq!(tag.directive_form, Some(DirectiveForm::Container));
3584
3585 assert_eq!(tag.origin, Some(ContainerOrigin::Element));
3587 assert_eq!(directive.origin, Some(ContainerOrigin::Directive));
3588 }
3589
3590 fn for_both_formats(src: &str, check: impl Fn(&mut Document, Format)) {
3594 for format in [Format::Markdown, Format::Djot] {
3595 let mut doc = Document::parse(src.as_bytes(), format).expect("parse");
3596 check(&mut doc, format);
3597 }
3598 }
3599
3600 #[test]
3601 fn marker_span_is_what_a_rich_view_hides() {
3602 for_both_formats("> - [x] done\n", |doc, format| {
3603 let nodes = doc.nodes().expect("nodes");
3604 let quote = nodes
3605 .iter()
3606 .find(|n| n.kind == Kind::BlockQuote)
3607 .expect("a block quote");
3608 let item = nodes
3609 .iter()
3610 .find(|n| n.kind == Kind::TaskListItem)
3611 .expect("a task item");
3612
3613 assert_eq!(quote.marker_span, Some(0..2), "{format:?}");
3617 assert_eq!(item.marker_span, Some(2..8), "{format:?}");
3618
3619 assert_eq!(quote.content_span, Some(quote.span.clone()), "{format:?}");
3623
3624 let para = nodes
3626 .iter()
3627 .find(|n| n.kind == Kind::Para)
3628 .expect("a paragraph");
3629 assert_eq!(para.marker_span, None, "{format:?}");
3630 });
3631 }
3632
3633 #[test]
3634 fn line_prefix_assembles_every_marker_on_the_line() {
3635 for_both_formats("> - [x] done\n", |doc, format| {
3636 assert_eq!(doc.line_prefix(9).expect("prefix"), Some(0..8), "{format:?}");
3639 });
3640 }
3641
3642 #[test]
3643 fn line_prefix_is_none_on_a_continuation_line() {
3644 for_both_formats("> c\n> d\n", |doc, format| {
3650 assert_eq!(doc.line_prefix(2).expect("prefix"), Some(0..2), "{format:?}");
3651 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
3652 });
3653 }
3654
3655 #[test]
3656 fn a_caret_at_a_blocks_end_is_in_that_block_in_both_formats() {
3657 for_both_formats("a\n\nb\n", |doc, format| {
3663 for offset in [0usize, 1, 3, 4] {
3664 let hit = doc
3665 .node_at_caret(offset)
3666 .expect("caret hit")
3667 .expect("some node");
3668 assert_eq!(hit.kind, Kind::Str, "{format:?} at {offset}");
3669 }
3670 for offset in [2usize, 5] {
3673 let hit = doc
3674 .node_at_caret(offset)
3675 .expect("caret hit")
3676 .expect("some node");
3677 assert_eq!(hit.kind, Kind::Doc, "{format:?} at {offset}");
3678 }
3679 });
3680 }
3681
3682 #[test]
3683 fn the_caret_chain_ends_at_the_node_the_scalar_call_returns() {
3684 for_both_formats("- a\n", |doc, format| {
3685 let hit = doc.node_at_caret(3).expect("hit").expect("some node");
3686 let chain = doc.ancestors_at_caret(3).expect("chain");
3687 assert_eq!(chain.last().map(|m| m.node_id), Some(hit.node_id), "{format:?}");
3688 assert!(
3691 chain.iter().any(|m| m.kind == Kind::ListItem),
3692 "{format:?}: chain should reach the list item"
3693 );
3694 });
3695 }
3696
3697 #[test]
3698 fn continuation_prefix_repeats_a_quote_and_indents_past_an_item() {
3699 for_both_formats("> - a\n", |doc, format| {
3700 assert_eq!(doc.line_prefix(4).expect("prefix"), Some(0..4), "{format:?}");
3705 let cont = doc.continuation_prefix(4).expect("continuation");
3706 assert_eq!(cont.text, "> ", "{format:?}");
3707 assert_eq!(cont.columns, 4, "{format:?}");
3708 });
3709 }
3710
3711 #[test]
3712 fn continuation_prefix_answers_on_a_line_that_opens_nothing() {
3713 for_both_formats("> c\n> d\n", |doc, format| {
3716 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
3717 assert_eq!(
3718 doc.continuation_prefix(6).expect("continuation").text,
3719 "> ",
3720 "{format:?}"
3721 );
3722 });
3723 }
3724
3725 #[test]
3726 fn continuation_prefix_takes_an_ordered_markers_own_width() {
3727 for_both_formats("10. x\n", |doc, format| {
3730 assert_eq!(
3731 doc.continuation_prefix(4).expect("continuation").columns,
3732 4,
3733 "{format:?}"
3734 );
3735 });
3736 for_both_formats("1. x\n", |doc, format| {
3737 assert_eq!(
3738 doc.continuation_prefix(3).expect("continuation").columns,
3739 3,
3740 "{format:?}"
3741 );
3742 });
3743 }
3744
3745 #[test]
3746 fn a_blank_line_keeps_a_quote_alive_and_drops_an_items_indent() {
3747 for_both_formats("> - a\n", |doc, format| {
3748 let blank = doc.blank_line_prefix(4).expect("blank");
3749 assert_eq!(blank.text, ">", "{format:?}");
3752 assert_eq!(blank.columns, 1, "{format:?}");
3753 });
3754 for_both_formats("- a\n", |doc, format| {
3757 assert_eq!(doc.blank_line_prefix(3).expect("blank").text, "", "{format:?}");
3758 });
3759 }
3760
3761 #[test]
3762 fn a_prefix_column_count_is_not_its_byte_length() {
3763 let mut doc = Document::parse("- x
3766".as_bytes(), Format::Markdown).expect("parse");
3767 let cont = doc.continuation_prefix(2).expect("continuation");
3768 assert_eq!(cont.columns, 4);
3769 }
3770
3771 #[test]
3772 fn set_block_opens_a_heading_on_a_blank_line() {
3773 for format in [Format::Markdown, Format::Djot] {
3774 let mut ed = Editor::new("a\n\n".as_bytes(), format).expect("editor");
3775 ed.set_block(3, BlockKind::Heading(2)).expect("set_block");
3776 assert_eq!(ed.source().expect("source"), b"a\n\n## ", "{format:?}");
3777 let nodes = ed.nodes().expect("nodes");
3781 assert!(
3782 nodes.iter().any(|n| n.kind == Kind::Heading),
3783 "{format:?}: should have parsed a heading"
3784 );
3785 }
3786 }
3787
3788 #[test]
3789 fn set_block_refuses_a_blank_line_inside_a_code_block() {
3790 for format in [Format::Markdown, Format::Djot] {
3794 let src = "```\nx\n\ny\n```\n";
3795 let mut ed = Editor::new(src.as_bytes(), format).expect("editor");
3796 let blank = src.find("\n\n").expect("a blank line") + 1;
3797 assert!(
3798 matches!(
3799 ed.set_block(blank, BlockKind::Heading(1)),
3800 Err(Error::NotEditable)
3801 ),
3802 "{format:?}"
3803 );
3804 assert_eq!(ed.source().expect("source"), src.as_bytes(), "{format:?}");
3805 }
3806 }
3807
3808 #[test]
3809 fn task_items_report_their_checkbox_state() {
3810 for_both_formats("- [ ] a\n- [x] b\n- [X] c\n- d\n", |doc, format| {
3814 let nodes = doc.nodes().expect("nodes");
3815 let states: Vec<Option<bool>> = nodes
3816 .iter()
3817 .filter(|n| matches!(n.kind, Kind::TaskListItem | Kind::ListItem))
3818 .map(|n| n.checked)
3819 .collect();
3820 assert_eq!(
3821 states,
3822 vec![Some(false), Some(true), Some(true), None],
3823 "{format:?}"
3824 );
3825
3826 for n in nodes.iter().filter(|n| n.kind == Kind::Para) {
3829 assert_eq!(n.checked, None, "{format:?}");
3830 }
3831 });
3832 }
3833
3834 #[test]
3835 fn an_editor_reaches_the_caret_reads_through_its_document_view() {
3836 let mut ed = Editor::new("- a\n".as_bytes(), Format::Markdown).expect("editor");
3841 let mut view = ed.document().expect("document view");
3842
3843 assert_eq!(view.line_prefix(3).expect("prefix"), Some(0..2));
3844 let hit = view.node_at_caret(3).expect("hit").expect("some node");
3845 assert_eq!(hit.kind, Kind::Str);
3846 }
3847
3848 #[test]
3849 fn container_origin_is_none_for_non_containers() {
3850 let mut ed = Editor::new("# hi\n\npara\n".as_bytes(), Format::Markdown).expect("editor");
3853 for n in ed.nodes().expect("nodes") {
3854 assert_eq!(n.origin, None, "{} should carry no origin", n.kind);
3855 }
3856 }
3857
3858 #[test]
3859 fn flat_nodes_expose_directive_name_and_form() {
3860 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
3866 let mut ed = Editor::new_ext(
3867 src.as_bytes(),
3868 Format::Markdown,
3869 MarkdownExtensions {
3870 directives: true,
3871 ..Default::default()
3872 },
3873 )
3874 .expect("editor");
3875 let nodes = ed.nodes().expect("nodes");
3876
3877 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
3878 .iter()
3879 .filter(|n| n.kind == Kind::Container)
3880 .map(|n| (n.name.as_deref(), n.directive_form))
3881 .collect();
3882 assert_eq!(
3883 forms,
3884 vec![
3885 (Some("note"), Some(DirectiveForm::Container)),
3886 (Some("embed"), Some(DirectiveForm::Leaf)),
3887 (Some("abbr"), Some(DirectiveForm::Text)),
3888 ]
3889 );
3890
3891 let embed = nodes
3894 .iter()
3895 .find(|n| n.name.as_deref() == Some("embed"))
3896 .expect("embed");
3897 assert_eq!(
3898 embed.attrs,
3899 vec![("src".to_string(), Some("demo.html".to_string()))]
3900 );
3901 let para = nodes.iter().find(|n| n.kind == Kind::Para).expect("a para");
3902 assert!(para.directive_form.is_none() && para.name.is_none());
3903 }
3904
3905 #[test]
3906 fn editor_insert_child_and_delete() {
3907 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
3908 ed.insert_child("0", 1, "<b/>").expect("insert_child");
3909 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
3910 ed.delete("0.1").expect("delete");
3911 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
3912 }
3913
3914 #[test]
3915 fn editor_edits_by_selector() {
3916 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
3917 ed.replace("heading(\"Two\")", "## Renamed")
3918 .expect("replace");
3919 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
3920 }
3921
3922 #[test]
3923 fn editor_locator_errors_are_distinct() {
3924 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
3925 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
3926 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
3927 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
3928 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
3930 }
3931
3932 #[test]
3933 fn editor_reparse_break_rolls_back() {
3934 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
3935 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
3936 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
3937 }
3938
3939 #[test]
3940 fn editor_leaf_content_is_not_editable() {
3941 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
3942 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
3943 }
3944
3945 #[test]
3946 fn editor_query_reflects_current_tree() {
3947 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
3948 ed.insert_child("0", 1, "<b/>").expect("insert_child");
3949 assert_eq!(ed.query("element").expect("query").len(), 3);
3951 let json = ed.ast_json().expect("ast_json");
3952 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
3953 }
3954
3955 #[test]
3958 fn editor_edit_range_types_backspaces_and_reports_change() {
3959 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
3960
3961 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
3963 assert_eq!(ed.source_str().unwrap(), "aXb\n");
3964 assert_eq!(c.old, 1..1);
3965 assert_eq!(c.new, 1..2);
3966 assert_eq!(c.delta(), 1);
3967
3968 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
3970 assert_eq!(ed.source_str().unwrap(), "ab\n");
3971 assert_eq!(c2.old, 1..2);
3972 assert_eq!(c2.new, 1..1);
3973 assert_eq!(c2.delta(), -1);
3974 }
3975
3976 #[test]
3977 fn editor_edit_range_rejects_bad_ranges() {
3978 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
3979 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"); }
3983
3984 #[test]
3985 fn editor_last_change_reports_locator_ops_too() {
3986 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
3987 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed")
3990 .expect("replace");
3991 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
3992 let c = ed.last_change().expect("a change was recorded");
3993 assert_eq!(c.old, 7..13);
3995 assert_eq!(c.new, 7..17);
3996 }
3997
3998 #[test]
3999 fn editor_nodes_is_a_walkable_flat_tree() {
4000 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4001 let nodes = ed.nodes().expect("nodes");
4002 assert!(!nodes.is_empty());
4003
4004 for (i, n) in nodes.iter().enumerate() {
4006 assert_eq!(n.id, NodeId(i as u32));
4007 }
4008 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
4010 assert_eq!(roots.len(), 1);
4011 assert_eq!(roots[0].kind, Kind::Doc);
4012
4013 let heading = nodes
4015 .iter()
4016 .find(|n| n.kind == Kind::Heading)
4017 .expect("a heading");
4018 assert_eq!(heading.level, Some(1));
4019 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
4020
4021 assert_eq!(heading.head, None);
4023 assert_eq!(heading.alignment, None);
4024
4025 for n in nodes.iter().filter(|n| n.parent.is_some()) {
4028 let p = &nodes[n.parent.unwrap().0 as usize];
4029 let mut kid = p.first_child;
4030 let mut seen = false;
4031 while let Some(NodeId(k)) = kid {
4032 if k == n.id.0 {
4033 seen = true;
4034 break;
4035 }
4036 kid = nodes[k as usize].next_sibling;
4037 }
4038 assert!(
4039 seen,
4040 "node {:?} not found among its parent's children",
4041 n.id
4042 );
4043 }
4044 }
4045
4046 #[test]
4047 fn editor_child_spans_and_subtree_agree_with_nodes() {
4048 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
4049 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4050 let all = ed.nodes().expect("nodes");
4051 let doc = all.iter().find(|n| n.kind == Kind::Doc).expect("doc");
4052
4053 let top = ed.child_spans(None).expect("child_spans");
4056 let mut want = Vec::new();
4057 let mut c = doc.first_child;
4058 while let Some(id) = c {
4059 want.push(id);
4060 c = all[id.0 as usize].next_sibling;
4061 }
4062 assert_eq!(top.len(), want.len(), "top-level count");
4063 for (m, id) in top.iter().zip(&want) {
4064 assert_eq!(m.node_id, id.0, "child id");
4065 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
4066 assert_eq!(m.span, all[id.0 as usize].span, "child span");
4067 }
4068 assert!(
4070 src[top[0].span.clone()].starts_with('#'),
4071 "first block is the heading"
4072 );
4073
4074 let list = top
4076 .iter()
4077 .find(|m| {
4078 matches!(
4079 m.kind,
4080 Kind::BulletList | Kind::OrderedList | Kind::TaskList
4081 )
4082 })
4083 .expect("a list");
4084 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
4085 assert_eq!(items.len(), 2);
4086 assert!(
4087 items.iter().all(|m| m.kind == Kind::ListItem),
4088 "items: {items:?}"
4089 );
4090
4091 let para = top
4093 .iter()
4094 .find(|m| m.kind == Kind::Para)
4095 .expect("a para")
4096 .node_id;
4097 let sub = ed.subtree(NodeId(para)).expect("subtree");
4098 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
4099 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
4100 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
4101 assert_eq!(sub[0].kind, Kind::Para);
4102 for (i, n) in sub.iter().enumerate() {
4103 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
4104 for link in [n.parent, n.first_child, n.next_sibling]
4105 .into_iter()
4106 .flatten()
4107 {
4108 assert!(
4109 (link.0 as usize) < sub.len(),
4110 "link {link:?} escapes the subtree"
4111 );
4112 }
4113 }
4114 assert!(
4115 src[sub[0].span.clone()].starts_with("Hello"),
4116 "absolute span: {:?}",
4117 &src[sub[0].span.clone()]
4118 );
4119
4120 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<Kind> {
4122 let mut out = Vec::new();
4123 let mut stack = vec![root];
4124 while let Some(id) = stack.pop() {
4125 let n = &all[id.0 as usize];
4126 out.push(n.kind.clone());
4127 let mut c = n.first_child;
4128 while let Some(cid) = c {
4129 stack.push(cid);
4130 c = all[cid.0 as usize].next_sibling;
4131 }
4132 }
4133 out
4134 }
4135 let mut want_kinds = arena_kinds(&all, NodeId(para));
4136 let mut got_kinds: Vec<Kind> = sub.iter().map(|n| n.kind.clone()).collect();
4137 want_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4141 got_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4142 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
4143
4144 assert!(matches!(
4146 ed.subtree(NodeId(9999)),
4147 Err(Error::InvalidArgument)
4148 ));
4149 }
4150
4151 #[test]
4152 fn flat_nodes_carry_table_head_and_alignment() {
4153 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
4157 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4158 let nodes = ed.nodes().expect("nodes");
4159
4160 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Row).collect();
4161 assert_eq!(rows.len(), 2, "a header row and one body row");
4162 assert_eq!(rows[0].head, Some(true), "first row is the header");
4163 assert_eq!(rows[1].head, Some(false), "second row is a body row");
4164
4165 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Cell).collect();
4166 assert_eq!(cells.len(), 4);
4167 assert_eq!(cells[0].alignment, Some(Alignment::Left));
4169 assert_eq!(cells[1].alignment, Some(Alignment::Right));
4170 assert_eq!(cells[2].alignment, Some(Alignment::Left));
4171 assert_eq!(cells[3].alignment, Some(Alignment::Right));
4172 assert_eq!(cells[0].head, Some(true));
4174 assert_eq!(cells[2].head, Some(false));
4175
4176 let mut plain =
4179 Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
4180 let pnodes = plain.nodes().expect("nodes");
4181 let pcell = pnodes
4182 .iter()
4183 .find(|n| n.kind == Kind::Cell)
4184 .expect("a cell");
4185 assert_eq!(pcell.alignment, Some(Alignment::Default));
4186 }
4187
4188 #[test]
4189 fn cell_extent_reports_merged_cells_and_nothing_else() {
4190 let src = "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td><td>b</td></tr></table>";
4191 let mut doc = Document::parse_str(src, Format::Html).expect("parse");
4192 let cells: Vec<NodeId> = doc
4193 .nodes()
4194 .expect("nodes")
4195 .iter()
4196 .filter(|n| n.kind == Kind::Cell)
4197 .map(|n| n.id)
4198 .collect();
4199 assert_eq!(cells.len(), 2);
4200 assert_eq!(doc.cell_extent(cells[0]).expect("extent"), Some((2, 3)));
4201 assert_eq!(doc.cell_extent(cells[1]).expect("extent"), Some((1, 1)));
4203
4204 let mut pipe =
4206 Document::parse_str("| a |\n| --- |\n| b |\n", Format::Markdown).expect("parse");
4207 let pipe_cell = pipe
4208 .nodes()
4209 .expect("nodes")
4210 .iter()
4211 .find(|n| n.kind == Kind::Cell)
4212 .expect("a cell")
4213 .id;
4214 assert_eq!(pipe.cell_extent(pipe_cell).expect("extent"), Some((1, 1)));
4215
4216 let root = NodeId(0);
4218 assert_eq!(pipe.cell_extent(root).expect("extent"), None);
4219 }
4220
4221 #[test]
4222 fn builder_add_cell_spanning_renders_colspan_and_rowspan() {
4223 let mut b = Builder::new().expect("builder");
4224 let wide_text = b.add_text(TextKind::Str, "wide").expect("str");
4225 let wide = b
4226 .add_cell_spanning(false, Alignment::Default, 2, 3)
4227 .expect("cell");
4228 b.set_children(wide, &[wide_text]).expect("children");
4229 let plain_text = b.add_text(TextKind::Str, "one").expect("str");
4230 let plain = b.add_cell(false, Alignment::Default).expect("cell");
4231 b.set_children(plain, &[plain_text]).expect("children");
4232 let row = b.add_row(false).expect("row");
4233 b.set_children(row, &[wide, plain]).expect("children");
4234 let table = b.add(VoidKind::Table).expect("table");
4235 b.set_children(table, &[row]).expect("children");
4236
4237 let html = String::from_utf8(b.render_html(table).expect("html")).expect("utf-8");
4238 assert!(
4239 html.contains("<td colspan=\"2\" rowspan=\"3\">wide</td>"),
4240 "{html}"
4241 );
4242 assert!(html.contains("<td>one</td>"), "{html}");
4244
4245 assert!(matches!(
4247 b.add_cell_spanning(false, Alignment::Default, 0, 1),
4248 Err(Error::InvalidArgument)
4249 ));
4250 }
4251
4252 #[test]
4253 fn editor_node_at_and_ancestors_hit_test_offsets() {
4254 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4255
4256 let m = ed
4258 .node_at(2)
4259 .expect("node_at")
4260 .expect("a node covers offset 2");
4261 assert!(m.span.contains(&2));
4262
4263 let chain = ed.ancestors_at(2).expect("ancestors_at");
4265 assert!(!chain.is_empty());
4266 assert_eq!(chain[0].kind, Kind::Doc);
4267 assert_eq!(chain.last().unwrap().node_id, m.node_id);
4268
4269 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
4271 }
4272
4273 #[test]
4276 fn editor_wrap_and_toggle_inline_round_trip() {
4277 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4278
4279 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
4281 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
4282 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
4283
4284 ed.toggle_inline(4, 8, InlineKind::Strong)
4286 .expect("toggle off");
4287 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4288
4289 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
4291 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
4292 }
4293
4294 #[test]
4295 fn editor_inline_kind_support_is_format_specific() {
4296 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4298 assert_eq!(
4299 md.wrap_range(2, 6, InlineKind::Mark),
4300 Err(Error::UnsupportedFormat)
4301 );
4302
4303 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4305 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
4306 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
4307 }
4308
4309 #[test]
4310 fn editor_toggle_strips_verbatim_via_content_span() {
4311 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
4312 ed.toggle_inline(2, 8, InlineKind::Verbatim)
4314 .expect("toggle code off");
4315 assert_eq!(ed.source_str().unwrap(), "a code b\n");
4316
4317 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
4320 ed2.toggle_inline(2, 7, InlineKind::Verbatim)
4321 .expect("toggle multi off");
4322 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
4323 }
4324
4325 #[test]
4326 fn editor_set_block_switches_para_and_heading_levels() {
4327 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
4328
4329 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
4331 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
4332
4333 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
4335 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
4336
4337 ed.set_block(2, BlockKind::Paragraph).expect("to para");
4339 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
4340 }
4341
4342 #[test]
4343 fn editor_set_block_rejects_bad_level_and_format() {
4344 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4345 assert_eq!(
4346 md.set_block(0, BlockKind::Heading(9)),
4347 Err(Error::InvalidArgument)
4348 );
4349
4350 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4351 assert_eq!(
4352 xml.set_block(1, BlockKind::Heading(1)),
4353 Err(Error::UnsupportedFormat)
4354 );
4355 }
4356
4357 #[test]
4358 fn editor_toggle_block_container_round_trips() {
4359 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
4360
4361 let c = ed
4362 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
4363 .expect("quote on");
4364 assert_eq!(ed.source_str().unwrap(), "> a\n");
4365 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
4366
4367 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4368 .expect("quote off");
4369 assert_eq!(ed.source_str().unwrap(), "a\n");
4370 }
4371
4372 #[test]
4373 fn editor_toggle_block_container_nests_a_partial_selection() {
4374 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
4375
4376 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4379 .expect("nest");
4380 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
4381
4382 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
4384 .expect("peel");
4385 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
4386 }
4387
4388 #[test]
4389 fn editor_toggle_block_container_numbers_and_converts_lists() {
4390 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
4391
4392 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
4394 .expect("ordered on");
4395 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
4396
4397 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
4399 .expect("convert");
4400 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
4401 }
4402
4403 #[test]
4404 fn editor_toggle_block_container_rejects_unspellable_format() {
4405 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4406 assert_eq!(
4407 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
4408 Err(Error::UnsupportedFormat)
4409 );
4410 }
4411
4412 #[test]
4413 fn editor_insert_link_wraps_and_repoints() {
4414 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4415
4416 ed.insert_link(2, 6, "http://x.dev").expect("link");
4417 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
4418
4419 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
4421 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
4422 }
4423
4424 #[test]
4425 fn editor_insert_link_repoints_an_autolink() {
4426 for format in [Format::Markdown, Format::Djot] {
4431 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
4432 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
4433 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
4434
4435 let nodes = ed.nodes().expect("nodes");
4437 let url = nodes
4438 .iter()
4439 .find(|n| n.kind == Kind::Url)
4440 .expect("still an autolink");
4441 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
4442 assert!(!nodes.iter().any(|n| n.kind == Kind::Link));
4443 }
4444 }
4445
4446 #[test]
4447 fn editor_insert_link_escapes_the_destination() {
4448 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4451 dj.insert_link(0, 1, "a)b").expect("link");
4452 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
4453
4454 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4458 md.insert_link(0, 1, "a b").expect("link");
4459 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
4460
4461 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
4462 dj2.insert_link(0, 1, "a b").expect("link");
4463 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
4464 }
4465
4466 #[test]
4467 fn editor_insert_image_escapes_the_destination_per_format() {
4468 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4471 md.insert_image(0, 1, "my cat.png").expect("image");
4472 assert_eq!(md.source_str().unwrap(), "\n");
4473
4474 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4475 dj.insert_image(0, 1, "my cat.png").expect("image");
4476 assert_eq!(dj.source_str().unwrap(), "\n");
4477
4478 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
4480 paren.insert_image(0, 1, "a)b.png").expect("image");
4481 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
4482 }
4483
4484 #[test]
4485 fn editor_insert_image_keeps_an_empty_alt_empty() {
4486 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4489 ed.insert_image(1, 1, "cat.png").expect("image");
4490 assert_eq!(ed.source_str().unwrap(), "ab\n");
4491 }
4492
4493 #[test]
4494 fn editor_insert_image_rejects_a_newline_destination() {
4495 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
4496 assert_eq!(
4497 ed.insert_image(0, 1, "a\nb.png"),
4498 Err(Error::InvalidArgument)
4499 );
4500
4501 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4502 assert_eq!(
4503 xml.insert_image(3, 5, "x.png"),
4504 Err(Error::UnsupportedFormat)
4505 );
4506 }
4507
4508 #[test]
4509 fn editor_insert_link_rejects_a_newline_destination() {
4510 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
4511 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
4512
4513 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4514 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
4515 }
4516
4517 #[test]
4518 fn editor_insert_literal_keeps_typed_specials_literal() {
4519 for format in [Format::Markdown, Format::Djot] {
4520 let mut ed = Editor::new_str("z\n", format).expect("editor");
4521 ed.insert_literal(0, "*hi*").expect("literal");
4523
4524 let nodes = ed.nodes().expect("nodes");
4526 assert!(
4527 !nodes
4528 .iter()
4529 .any(|n| n.kind == Kind::Emph || n.kind == Kind::Strong)
4530 );
4531 let text: String = nodes
4532 .iter()
4533 .filter(|n| n.kind == Kind::Str)
4534 .filter_map(|n| n.text.clone())
4535 .collect();
4536 assert_eq!(text, "*hi*z");
4537 }
4538 }
4539
4540 #[test]
4541 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
4542 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
4544 ed.insert_literal(1, "# ").expect("literal");
4545 assert_eq!(ed.source_str().unwrap(), "a# z\n");
4546
4547 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
4549 ed2.insert_literal(0, "# ").expect("literal");
4550 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
4551 assert!(
4552 !ed2.nodes()
4553 .expect("nodes")
4554 .iter()
4555 .any(|n| n.kind == Kind::Heading)
4556 );
4557 }
4558
4559 #[test]
4560 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
4561 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4562 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
4563
4564 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4565 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
4566 }
4567
4568 #[test]
4569 fn editor_insert_line_break_splices_in_cell_br() {
4570 let mut ed =
4571 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
4572 ed.insert_line_break(3).expect("line break");
4574 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
4575 let nodes = ed.nodes().expect("nodes");
4577 assert!(nodes.iter().any(|n| n.kind == Kind::HardBreak));
4578 assert!(!nodes.iter().any(|n| n.kind == Kind::RawInline));
4579 }
4580
4581 #[test]
4582 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
4583 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
4585 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
4586
4587 let mut dj = Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
4589 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
4590
4591 let mut ed =
4593 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
4594 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
4595 }
4596
4597 #[test]
4598 fn editor_insert_thematic_break_is_blank_separated_per_format() {
4599 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
4603 md.insert_thematic_break(0).expect("rule");
4604 assert_eq!(md.source_str().unwrap(), "a\n\n---\n");
4605 let nodes = md.nodes().expect("nodes");
4606 assert!(nodes.iter().any(|n| n.kind == Kind::ThematicBreak));
4607 assert!(!nodes.iter().any(|n| n.kind == Kind::Heading));
4608
4609 let mut dj = Editor::new_str("a\n", Format::Djot).expect("editor");
4612 dj.insert_thematic_break(0).expect("rule");
4613 assert_eq!(dj.source_str().unwrap(), "a\n\n* * *\n");
4614
4615 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4616 assert_eq!(xml.insert_thematic_break(3), Err(Error::UnsupportedFormat));
4617 }
4618
4619 #[test]
4620 fn editor_split_block_keeps_both_halves_the_same_kind() {
4621 let mut item = Editor::new_str("- this is a list item\n", Format::Markdown).expect("editor");
4624 item.split_block(10).expect("split");
4625 assert_eq!(item.source_str().unwrap(), "- this is \n- a list item\n");
4626 let nodes = item.nodes().expect("nodes");
4627 assert_eq!(nodes.iter().filter(|n| n.kind == Kind::ListItem).count(), 2);
4628
4629 let mut tail = Editor::new_str("- a\n", Format::Markdown).expect("editor");
4631 tail.split_block(3).expect("split");
4632 assert_eq!(tail.source_str().unwrap(), "- a\n- \n");
4633
4634 let mut para = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4636 para.split_block(1).expect("split");
4637 assert_eq!(para.source_str().unwrap(), "a\n\nb\n");
4638
4639 let mut table =
4641 Editor::new_str("| a | b |\n|---|---|\n| c | d |\n", Format::Markdown).expect("editor");
4642 assert_eq!(table.split_block(3), Err(Error::NotEditable));
4643
4644 let mut empty = Editor::new_str("", Format::Markdown).expect("editor");
4645 assert_eq!(empty.split_block(0), Err(Error::NotFound));
4646 }
4647
4648 #[test]
4649 fn editor_toggle_code_block_round_trips_and_measures_the_fence() {
4650 let mut ed = Editor::new_str("a\n", Format::Markdown).expect("editor");
4651 ed.toggle_code_block(0, 1, Some("zig")).expect("fence");
4652 assert_eq!(ed.source_str().unwrap(), "```zig\na\n```\n");
4653 let nodes = ed.nodes().expect("nodes");
4654 assert!(nodes.iter().any(|n| n.kind == Kind::CodeBlock));
4655
4656 ed.toggle_code_block(0, 0, None).expect("unfence");
4657 assert_eq!(ed.source_str().unwrap(), "a\n");
4658
4659 let mut runs = Editor::new_str("a ``` b\n", Format::Markdown).expect("editor");
4662 runs.toggle_code_block(0, 7, None).expect("fence");
4663 assert_eq!(runs.source_str().unwrap(), "````\na ``` b\n````\n");
4664 }
4665
4666 #[test]
4667 fn editor_toggle_code_block_refuses_inside_a_list_item() {
4668 let mut ed = Editor::new_str("- a\n- b\n", Format::Markdown).expect("editor");
4671 assert_eq!(ed.toggle_code_block(2, 3, None), Err(Error::NotEditable));
4672 assert_eq!(ed.source_str().unwrap(), "- a\n- b\n");
4673 }
4674
4675 #[test]
4676 fn editor_set_code_language_retags_clears_and_refuses() {
4677 let mut ed = Editor::new_str("```zig\na\n```\n", Format::Markdown).expect("editor");
4678 ed.set_code_language(0, Some("rust")).expect("retag");
4679 assert_eq!(ed.source_str().unwrap(), "```rust\na\n```\n");
4680
4681 ed.set_code_language(0, None).expect("clear");
4684 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
4685 ed.set_code_language(0, Some("")).expect("empty");
4686 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
4687
4688 assert_eq!(
4691 ed.set_code_language(0, Some("a b")),
4692 Err(Error::InvalidArgument)
4693 );
4694 let mut dj = Editor::new_str("```\na\n```\n", Format::Djot).expect("editor");
4696 dj.set_code_language(0, Some("a b"))
4697 .expect("djot info string");
4698 assert_eq!(dj.source_str().unwrap(), "```a b\na\n```\n");
4699
4700 let mut para = Editor::new_str("x\n", Format::Markdown).expect("editor");
4701 assert_eq!(para.set_code_language(0, Some("zig")), Err(Error::NotFound));
4702 }
4703
4704 #[test]
4705 fn editor_task_checkbox_gestures() {
4706 let mut ed = Editor::new_str("- a\n", Format::Markdown).expect("editor");
4707
4708 ed.toggle_task_item(2).expect("add box");
4711 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
4712 assert!(
4713 ed.nodes()
4714 .unwrap()
4715 .iter()
4716 .any(|n| n.kind == Kind::TaskListItem)
4717 );
4718
4719 ed.set_task_checked(6, true).expect("tick");
4720 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
4721 ed.set_task_checked(6, true).expect("no-op");
4723 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
4724
4725 ed.toggle_task_checked(6).expect("flip");
4726 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
4727
4728 ed.toggle_task_item(6).expect("remove box");
4729 assert_eq!(ed.source_str().unwrap(), "- a\n");
4730
4731 assert_eq!(ed.set_task_checked(2, true), Err(Error::NotEditable));
4734 let mut para = Editor::new_str("a\n", Format::Markdown).expect("editor");
4736 assert_eq!(para.toggle_task_item(0), Err(Error::NotFound));
4737 }
4738
4739 #[test]
4740 fn editor_insert_footnote_writes_both_halves_as_one_edit() {
4741 for format in [Format::Markdown, Format::Djot] {
4742 let mut ed = Editor::new_str("see\n", format).expect("editor");
4743 ed.insert_footnote(3, "a").expect("footnote");
4744 assert_eq!(ed.source_str().unwrap(), "see[^a]\n\n[^a]: \n");
4745
4746 let nodes = ed.nodes().expect("nodes");
4748 assert!(nodes.iter().any(|n| n.kind == Kind::FootnoteReference));
4749 assert!(nodes.iter().any(|n| n.kind == Kind::Footnote));
4750
4751 ed.undo().expect("undo");
4753 assert_eq!(ed.source_str().unwrap(), "see\n");
4754 }
4755 }
4756
4757 #[test]
4758 fn editor_insert_footnote_reuses_an_existing_definition() {
4759 let mut ed = Editor::new_str("see\n", Format::Markdown).expect("editor");
4760 ed.insert_footnote(3, "a").expect("first");
4761 ed.insert_footnote(7, "a").expect("second reference");
4762 assert_eq!(ed.source_str().unwrap(), "see[^a][^a]\n\n[^a]: \n");
4763 let defs = ed
4764 .nodes()
4765 .unwrap()
4766 .iter()
4767 .filter(|n| n.kind == Kind::Footnote)
4768 .count();
4769 assert_eq!(defs, 1);
4770
4771 assert_eq!(ed.insert_footnote(3, ""), Err(Error::InvalidArgument));
4772 assert_eq!(ed.insert_footnote(3, "a]b"), Err(Error::InvalidArgument));
4773 }
4774
4775 #[test]
4776 fn editor_undo_redo_round_trip() {
4777 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
4778 ed.edit_range(5, 5, "!").expect("edit");
4779 assert_eq!(ed.source_str().unwrap(), "hello!\n");
4780
4781 let change = ed.undo().expect("undo ok").expect("something to undo");
4782 assert_eq!(ed.source_str().unwrap(), "hello\n");
4783 assert_eq!(change.new.end, 5);
4784 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
4785
4786 ed.redo().expect("redo ok").expect("something to redo");
4787 assert_eq!(ed.source_str().unwrap(), "hello!\n");
4788 }
4789
4790 #[test]
4791 fn editor_coalesce_folds_a_run() {
4792 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
4793 ed.edit_range(0, 0, "a").expect("edit");
4794 ed.edit_range(1, 1, "b").expect("edit");
4795 ed.coalesce_last_undo().expect("coalesce");
4796 assert_eq!(ed.source_str().unwrap(), "ab\n");
4797 ed.undo().expect("undo ok").expect("something to undo");
4799 assert_eq!(ed.source_str().unwrap(), "\n");
4800 assert!(ed.undo().expect("undo ok").is_none());
4801 }
4802
4803 #[test]
4804 fn editor_revision_bumps_per_successful_mutation() {
4805 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
4806 assert_eq!(ed.revision(), 0);
4807 ed.edit_range(1, 1, "y").expect("edit");
4808 assert_eq!(ed.revision(), 1);
4809
4810 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
4812 assert_eq!(xml.revision(), 0);
4813 assert!(xml.replace_content("0", "<b>").is_err());
4814 assert_eq!(xml.revision(), 0);
4815
4816 ed.undo().expect("undo ok").expect("something to undo");
4818 assert_eq!(ed.revision(), 2);
4819 ed.redo().expect("redo ok").expect("something to redo");
4820 assert_eq!(ed.revision(), 3);
4821 }
4822
4823 #[test]
4824 fn editor_dirty_range_tracks_and_clears() {
4825 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
4826 assert_eq!(ed.dirty_range(), None);
4828
4829 ed.edit_range(2, 2, "XY").expect("edit");
4831 assert_eq!(ed.dirty_range(), Some(2..4));
4832
4833 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
4837 assert!(
4838 d.start <= 2 && d.end >= 10,
4839 "range {d:?} must cover both edits"
4840 );
4841
4842 let rev = ed.revision();
4844 ed.clear_dirty();
4845 assert_eq!(ed.dirty_range(), None);
4846 assert_eq!(ed.revision(), rev);
4847
4848 ed.undo().expect("undo ok").expect("something to undo");
4850 assert!(ed.dirty_range().is_some());
4851 }
4852
4853 #[test]
4854 fn editor_caret_blob_follows_undo_and_redo() {
4855 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
4856 assert!(ed.caret_blob().unwrap().is_empty());
4857
4858 ed.set_caret_blob(b"before").expect("set caret");
4860 ed.edit_range(5, 5, "!").expect("edit");
4861 assert!(ed.caret_blob().unwrap().is_empty());
4863 ed.set_caret_blob(b"after").expect("set caret");
4864
4865 ed.undo().expect("undo ok").expect("something to undo");
4867 assert_eq!(ed.source_str().unwrap(), "hello\n");
4868 assert_eq!(ed.caret_blob().unwrap(), b"before");
4869
4870 ed.redo().expect("redo ok").expect("something to redo");
4872 assert_eq!(ed.source_str().unwrap(), "hello!\n");
4873 assert_eq!(ed.caret_blob().unwrap(), b"after");
4874 }
4875
4876 #[test]
4877 fn editor_coalesced_run_keeps_the_pre_run_caret() {
4878 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
4879 ed.set_caret_blob(b"c0").expect("set caret");
4880 ed.edit_range(0, 0, "a").expect("edit");
4881 ed.set_caret_blob(b"c1").expect("set caret");
4882 ed.edit_range(1, 1, "b").expect("edit");
4883 ed.coalesce_last_undo().expect("coalesce");
4884 ed.set_caret_blob(b"c2").expect("set caret");
4885
4886 ed.undo().expect("undo ok").expect("something to undo");
4888 assert_eq!(ed.source_str().unwrap(), "\n");
4889 assert_eq!(ed.caret_blob().unwrap(), b"c0");
4890 }
4891
4892 #[test]
4893 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
4894 let mut ed = Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
4895 ed.renumber_ordered_lists(0).expect("renumber ok");
4896 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
4897 }
4898
4899 #[test]
4900 fn editor_renumber_ordered_lists_leaves_djot_prose_alone() {
4901 let src = "1. a\n 2. b\n2. c\n";
4904 let mut dj = Editor::new_str(src, Format::Djot).expect("editor");
4905 dj.renumber_ordered_lists(0).expect("renumber ok");
4906 assert_eq!(dj.source_str().unwrap(), src);
4907
4908 let mut md = Editor::new_str(src, Format::Markdown).expect("editor");
4909 md.renumber_ordered_lists(0).expect("renumber ok");
4910 assert_eq!(md.source_str().unwrap(), "1. a\n 1. b\n2. c\n");
4911 }
4912
4913 #[test]
4914 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
4915 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
4916 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
4917 }
4918
4919 #[test]
4920 fn editor_table_insert_row_and_set_alignment() {
4921 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
4922 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4923 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
4925 ed.source_str().unwrap(),
4926 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
4927 );
4928 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
4930 }
4931
4932 #[test]
4933 fn editor_table_edit_off_a_table_is_not_found() {
4934 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
4935 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
4936 }
4937
4938 #[test]
4939 fn editor_set_block_converts_setext_heading() {
4940 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
4942 ed.set_block(0, BlockKind::Heading(1))
4943 .expect("setext to atx");
4944 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
4945 }
4946
4947 #[test]
4948 fn editor_unwrap_and_smart_delete() {
4949 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
4950 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
4952
4953 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
4954 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
4956 }
4957
4958 #[test]
4959 fn editor_directives_require_the_extension_flag() {
4960 let src = ":::vis{.public}\nhi\n:::\n";
4961 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
4964 assert_eq!(plain.query("directive").expect("query").len(), 0);
4965 let mut ext = Editor::new_ext(
4967 src.as_bytes(),
4968 Format::Markdown,
4969 MarkdownExtensions {
4970 directives: true,
4971 ..Default::default()
4972 },
4973 )
4974 .expect("editor");
4975 assert_eq!(ext.query("directive").expect("query").len(), 1);
4976 }
4977
4978 #[test]
4979 fn document_html_elements_make_embedded_img_queryable() {
4980 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
4981 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
4983 assert_eq!(plain.query("image").expect("query").len(), 0);
4984 let mut ext = Document::parse_str_with(
4986 src,
4987 Format::Markdown,
4988 MarkdownExtensions {
4989 html_elements: true,
4990 ..Default::default()
4991 },
4992 )
4993 .expect("parse");
4994 let images = ext.query("image").expect("query");
4995 assert_eq!(images.len(), 1);
4996 assert_eq!(images[0].kind, Kind::Image);
4997 }
4998
4999 #[test]
5000 fn editor_filter_public_audience_view() {
5001 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
5002 let mut ed = Editor::new_ext(
5003 src.as_bytes(),
5004 Format::Markdown,
5005 MarkdownExtensions {
5006 directives: true,
5007 ..Default::default()
5008 },
5009 )
5010 .expect("editor");
5011 ed.filter(
5013 "directive[name=vis]",
5014 Some("directive[class~=public]"),
5015 true,
5016 )
5017 .expect("filter");
5018 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
5019 }
5020
5021 #[test]
5022 fn editor_filter_rejects_a_malformed_selector() {
5023 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
5024 assert_eq!(
5025 ed.filter("list >", None, false),
5026 Err(Error::InvalidArgument)
5027 );
5028 }
5029
5030 #[test]
5031 fn builder_builds_and_renders_a_document() {
5032 let mut b = Builder::new().expect("builder");
5033
5034 let title = b.add_text(TextKind::Str, "Title").unwrap();
5036 let heading = b.add_heading(1).unwrap();
5037 b.set_children(heading, &[title]).unwrap();
5038
5039 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
5040 let world = b.add_text(TextKind::Str, "world").unwrap();
5041 let emph = b.add(VoidKind::Emph).unwrap();
5042 b.set_children(emph, &[world]).unwrap();
5043 let para = b.add(VoidKind::Para).unwrap();
5044 b.set_children(para, &[hello, emph]).unwrap();
5045
5046 let doc = b.add(VoidKind::Doc).unwrap();
5047 b.set_children(doc, &[heading, para]).unwrap();
5048
5049 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
5050 assert!(html.contains("<h1>Title</h1>"), "{html}");
5051 assert!(html.contains("<em>world</em>"), "{html}");
5052
5053 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5054 assert!(md.contains("# Title"), "{md}");
5055 assert!(md.contains("*world*"), "{md}");
5056
5057 let matches = b.query(doc, "heading").unwrap();
5058 assert_eq!(matches.len(), 1);
5059 assert_eq!(matches[0].kind, Kind::Heading);
5060
5061 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
5062 assert!(json.contains("\"kind\": \"doc\""), "{json}");
5063 }
5064
5065 #[test]
5066 fn builder_element_with_attributes() {
5067 let mut b = Builder::new().expect("builder");
5068 let inner = b.add_text(TextKind::Str, "hi").unwrap();
5069 let el = b.add_element("section").unwrap();
5070 b.set_children(el, &[inner]).unwrap();
5071 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)])
5072 .unwrap();
5073
5074 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
5075 assert!(html.contains("<section"), "{html}");
5076 assert!(html.contains("class=\"note\""), "{html}");
5077 assert!(html.contains("hidden"), "{html}");
5078 }
5079
5080 #[test]
5081 fn builder_lists_round_trip_to_markdown() {
5082 let mut b = Builder::new().expect("builder");
5083
5084 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
5086 let one_para = b.add(VoidKind::Para).unwrap();
5087 b.set_children(one_para, &[one_txt]).unwrap();
5088 let one = b.add(VoidKind::ListItem).unwrap();
5089 b.set_children(one, &[one_para]).unwrap();
5090
5091 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
5092 let two_para = b.add(VoidKind::Para).unwrap();
5093 b.set_children(two_para, &[two_txt]).unwrap();
5094 let two = b.add(VoidKind::ListItem).unwrap();
5095 b.set_children(two, &[two_para]).unwrap();
5096
5097 let list = b
5098 .add_ordered_list(
5099 OrderedNumbering::Decimal,
5100 OrderedDelim::Period,
5101 true,
5102 Some(1),
5103 )
5104 .unwrap();
5105 b.set_children(list, &[one, two]).unwrap();
5106 let doc = b.add(VoidKind::Doc).unwrap();
5107 b.set_children(doc, &[list]).unwrap();
5108
5109 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5110 assert!(md.contains("1. one"), "{md}");
5111 assert!(md.contains("2. two"), "{md}");
5112 }
5113
5114 #[test]
5115 fn builder_rejects_invalid_kind_and_id() {
5116 let b = Builder::new().expect("builder");
5117 let mut id = 0u32;
5121 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
5122 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5123
5124 let mut ptr = std::ptr::null();
5126 let mut len = 0usize;
5127 let status =
5128 unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
5129 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5130 }
5131}