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,
42}
43
44impl From<Format> for ffi::TwigFormat {
45 fn from(value: Format) -> Self {
46 match value {
47 Format::Djot => ffi::TwigFormat::Djot,
48 Format::Markdown => ffi::TwigFormat::Markdown,
49 Format::Xml => ffi::TwigFormat::Xml,
50 Format::Html => ffi::TwigFormat::Html,
51 Format::Asciidoc => ffi::TwigFormat::Asciidoc,
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71#[non_exhaustive]
72pub enum Target {
73 Djot,
74 Markdown,
75 Xml,
76 Html,
77 Asciidoc,
81}
82
83impl Target {
84 pub fn as_format(self) -> Option<Format> {
91 match self {
92 Target::Djot => Some(Format::Djot),
93 Target::Markdown => Some(Format::Markdown),
94 Target::Xml => Some(Format::Xml),
95 Target::Html => Some(Format::Html),
96 Target::Asciidoc => Some(Format::Asciidoc),
97 }
98 }
99}
100
101impl From<Format> for Target {
105 fn from(value: Format) -> Self {
106 match value {
107 Format::Djot => Target::Djot,
108 Format::Markdown => Target::Markdown,
109 Format::Xml => Target::Xml,
110 Format::Html => Target::Html,
111 Format::Asciidoc => Target::Asciidoc,
112 }
113 }
114}
115
116impl From<Target> for ffi::TwigFormat {
117 fn from(value: Target) -> Self {
118 match value {
119 Target::Djot => ffi::TwigFormat::Djot,
120 Target::Markdown => ffi::TwigFormat::Markdown,
121 Target::Xml => ffi::TwigFormat::Xml,
122 Target::Html => ffi::TwigFormat::Html,
123 Target::Asciidoc => ffi::TwigFormat::Asciidoc,
124 }
125 }
126}
127
128#[derive(Clone, Debug, Eq, PartialEq, Hash)]
162#[non_exhaustive]
163pub enum Kind {
164 Doc,
166 Para,
168 Heading,
169 ThematicBreak,
170 Section,
171 CodeBlock,
172 RawBlock,
173 Metadata,
174 BlockQuote,
175 BulletList,
176 OrderedList,
177 TaskList,
178 DefinitionList,
179 LineBlock,
180 Table,
181 ListItem,
183 TaskListItem,
184 DefinitionListItem,
185 Term,
186 Definition,
187 Line,
188 Row,
189 Cell,
190 Column,
191 Caption,
192 Footnote,
193 Reference,
194 Citation,
195 Substitution,
196 Str,
198 SoftBreak,
199 HardBreak,
200 NonBreakingSpace,
201 RawInline,
202 SmartPunctuation,
203 Link,
204 Image,
205 Emph,
207 Strong,
208 Mark,
209 Superscript,
210 Subscript,
211 Insert,
212 Delete,
213 DoubleQuoted,
214 SingleQuoted,
215 Symb,
217 Verbatim,
218 InlineMath,
219 DisplayMath,
220 Url,
221 Email,
222 FootnoteReference,
223 CitationReference,
224 SubstitutionReference,
225 Container,
227 ProcessingInstruction,
228 Comment,
229 Doctype,
230 Cdata,
231 Other(String),
238}
239
240impl Kind {
241 pub fn as_str(&self) -> &str {
244 match self {
245 Kind::Doc => "doc",
246 Kind::Para => "para",
247 Kind::Heading => "heading",
248 Kind::ThematicBreak => "thematic_break",
249 Kind::Section => "section",
250 Kind::CodeBlock => "code_block",
251 Kind::RawBlock => "raw_block",
252 Kind::Metadata => "metadata",
253 Kind::BlockQuote => "block_quote",
254 Kind::BulletList => "bullet_list",
255 Kind::OrderedList => "ordered_list",
256 Kind::TaskList => "task_list",
257 Kind::DefinitionList => "definition_list",
258 Kind::LineBlock => "line_block",
259 Kind::Table => "table",
260 Kind::ListItem => "list_item",
261 Kind::TaskListItem => "task_list_item",
262 Kind::DefinitionListItem => "definition_list_item",
263 Kind::Term => "term",
264 Kind::Definition => "definition",
265 Kind::Line => "line",
266 Kind::Row => "row",
267 Kind::Cell => "cell",
268 Kind::Column => "column",
269 Kind::Caption => "caption",
270 Kind::Footnote => "footnote",
271 Kind::Reference => "reference",
272 Kind::Citation => "citation",
273 Kind::Substitution => "substitution",
274 Kind::Str => "str",
275 Kind::SoftBreak => "soft_break",
276 Kind::HardBreak => "hard_break",
277 Kind::NonBreakingSpace => "non_breaking_space",
278 Kind::RawInline => "raw_inline",
279 Kind::SmartPunctuation => "smart_punctuation",
280 Kind::Link => "link",
281 Kind::Image => "image",
282 Kind::Container => "container",
283 Kind::ProcessingInstruction => "processing_instruction",
284 Kind::Emph => "emph",
285 Kind::Strong => "strong",
286 Kind::Mark => "mark",
287 Kind::Superscript => "superscript",
288 Kind::Subscript => "subscript",
289 Kind::Insert => "insert",
290 Kind::Delete => "delete",
291 Kind::DoubleQuoted => "double_quoted",
292 Kind::SingleQuoted => "single_quoted",
293 Kind::Symb => "symb",
294 Kind::Verbatim => "verbatim",
295 Kind::InlineMath => "inline_math",
296 Kind::DisplayMath => "display_math",
297 Kind::Url => "url",
298 Kind::Email => "email",
299 Kind::FootnoteReference => "footnote_reference",
300 Kind::CitationReference => "citation_reference",
301 Kind::SubstitutionReference => "substitution_reference",
302 Kind::Comment => "comment",
303 Kind::Doctype => "doctype",
304 Kind::Cdata => "cdata",
305 Kind::Other(name) => name.as_str(),
306 }
307 }
308
309 pub fn is_unknown(&self) -> bool {
313 matches!(self, Kind::Other(_))
314 }
315}
316
317impl From<&str> for Kind {
318 fn from(name: &str) -> Self {
319 match name {
320 "doc" => Kind::Doc,
321 "para" => Kind::Para,
322 "heading" => Kind::Heading,
323 "thematic_break" => Kind::ThematicBreak,
324 "section" => Kind::Section,
325 "code_block" => Kind::CodeBlock,
326 "raw_block" => Kind::RawBlock,
327 "metadata" => Kind::Metadata,
328 "block_quote" => Kind::BlockQuote,
329 "bullet_list" => Kind::BulletList,
330 "ordered_list" => Kind::OrderedList,
331 "task_list" => Kind::TaskList,
332 "definition_list" => Kind::DefinitionList,
333 "line_block" => Kind::LineBlock,
334 "table" => Kind::Table,
335 "list_item" => Kind::ListItem,
336 "task_list_item" => Kind::TaskListItem,
337 "definition_list_item" => Kind::DefinitionListItem,
338 "term" => Kind::Term,
339 "definition" => Kind::Definition,
340 "line" => Kind::Line,
341 "row" => Kind::Row,
342 "cell" => Kind::Cell,
343 "column" => Kind::Column,
344 "caption" => Kind::Caption,
345 "footnote" => Kind::Footnote,
346 "reference" => Kind::Reference,
347 "citation" => Kind::Citation,
348 "substitution" => Kind::Substitution,
349 "str" => Kind::Str,
350 "soft_break" => Kind::SoftBreak,
351 "hard_break" => Kind::HardBreak,
352 "non_breaking_space" => Kind::NonBreakingSpace,
353 "raw_inline" => Kind::RawInline,
354 "smart_punctuation" => Kind::SmartPunctuation,
355 "link" => Kind::Link,
356 "image" => Kind::Image,
357 "container" => Kind::Container,
358 "processing_instruction" => Kind::ProcessingInstruction,
359 "emph" => Kind::Emph,
360 "strong" => Kind::Strong,
361 "mark" => Kind::Mark,
362 "superscript" => Kind::Superscript,
363 "subscript" => Kind::Subscript,
364 "insert" => Kind::Insert,
365 "delete" => Kind::Delete,
366 "double_quoted" => Kind::DoubleQuoted,
367 "single_quoted" => Kind::SingleQuoted,
368 "symb" => Kind::Symb,
369 "verbatim" => Kind::Verbatim,
370 "inline_math" => Kind::InlineMath,
371 "display_math" => Kind::DisplayMath,
372 "url" => Kind::Url,
373 "email" => Kind::Email,
374 "footnote_reference" => Kind::FootnoteReference,
375 "citation_reference" => Kind::CitationReference,
376 "substitution_reference" => Kind::SubstitutionReference,
377 "comment" => Kind::Comment,
378 "doctype" => Kind::Doctype,
379 "cdata" => Kind::Cdata,
380 other => Kind::Other(other.to_string()),
381 }
382 }
383}
384
385impl std::fmt::Display for Kind {
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 f.write_str(self.as_str())
388 }
389}
390
391#[derive(Clone, Debug, Eq, PartialEq)]
393pub struct QueryMatch {
394 pub node_id: u32,
396 pub span: Range<usize>,
398 pub content_span: Option<Range<usize>>,
401 pub kind: Kind,
404}
405
406#[derive(Clone, Debug, Eq, PartialEq)]
413pub struct Change {
414 pub old: Range<usize>,
415 pub new: Range<usize>,
416}
417
418impl Change {
419 pub fn delta(&self) -> isize {
421 self.new.len() as isize - self.old.len() as isize
422 }
423
424 fn from_ffi(c: ffi::TwigChange) -> Self {
425 Change {
426 old: c.old_span.start..c.old_span.end,
427 new: c.new_span.start..c.new_span.end,
428 }
429 }
430}
431
432#[derive(Clone, Debug, Eq, PartialEq)]
443#[non_exhaustive]
444pub struct FlatNode {
445 pub id: NodeId,
446 pub parent: Option<NodeId>,
447 pub first_child: Option<NodeId>,
448 pub next_sibling: Option<NodeId>,
449 pub span: Range<usize>,
450 pub content_span: Option<Range<usize>>,
451 pub level: Option<u32>,
453 pub kind: Kind,
454 pub text: Option<String>,
455 pub destination: Option<String>,
456 pub head: Option<bool>,
459 pub alignment: Option<Alignment>,
465 pub name: Option<String>,
477 pub directive_form: Option<DirectiveForm>,
492 pub origin: Option<ContainerOrigin>,
502 pub marker_span: Option<Range<usize>>,
521 pub checked: Option<bool>,
534 pub attrs: Vec<(String, Option<String>)>,
538}
539
540#[derive(Clone, Debug, Default, Eq, PartialEq)]
548pub struct LinePrefix {
549 pub text: String,
551 pub columns: usize,
553}
554
555#[derive(Clone, Copy, Debug, Eq, PartialEq)]
565pub enum InlineKind {
566 Strong,
567 Emph,
568 Verbatim,
569 Mark,
570 Superscript,
571 Subscript,
572 Insert,
573 Delete,
574}
575
576impl InlineKind {
577 fn to_c(self) -> c_int {
578 match self {
579 InlineKind::Strong => 0,
580 InlineKind::Emph => 1,
581 InlineKind::Verbatim => 2,
582 InlineKind::Mark => 3,
583 InlineKind::Superscript => 4,
584 InlineKind::Subscript => 5,
585 InlineKind::Insert => 6,
586 InlineKind::Delete => 7,
587 }
588 }
589}
590
591#[derive(Clone, Copy, Debug, Eq, PartialEq)]
593pub enum BlockKind {
594 Paragraph,
595 Heading(u32),
597}
598
599impl BlockKind {
600 fn to_c(self) -> (c_int, u32) {
602 match self {
603 BlockKind::Paragraph => (0, 0),
604 BlockKind::Heading(level) => (1, level),
605 }
606 }
607}
608
609#[derive(Clone, Copy, Debug, Eq, PartialEq)]
615pub enum BlockContainerKind {
616 BlockQuote,
617 BulletList,
618 OrderedList,
619}
620
621impl BlockContainerKind {
622 fn to_c(self) -> c_int {
623 match self {
624 BlockContainerKind::BlockQuote => 0,
625 BlockContainerKind::BulletList => 1,
626 BlockContainerKind::OrderedList => 2,
627 }
628 }
629}
630
631#[derive(Clone, Copy, Debug, Eq, PartialEq)]
645pub enum MarkColor {
646 Red,
647 Orange,
648 Yellow,
649 Green,
650 Blue,
651 Purple,
652 Brown,
653}
654
655impl MarkColor {
656 pub fn as_str(self) -> &'static str {
658 match self {
659 MarkColor::Red => "red",
660 MarkColor::Orange => "orange",
661 MarkColor::Yellow => "yellow",
662 MarkColor::Green => "green",
663 MarkColor::Blue => "blue",
664 MarkColor::Purple => "purple",
665 MarkColor::Brown => "brown",
666 }
667 }
668
669 pub fn from_str(s: &str) -> Option<Self> {
672 Some(match s {
673 "red" => MarkColor::Red,
674 "orange" => MarkColor::Orange,
675 "yellow" => MarkColor::Yellow,
676 "green" => MarkColor::Green,
677 "blue" => MarkColor::Blue,
678 "purple" => MarkColor::Purple,
679 "brown" => MarkColor::Brown,
680 _ => return None,
681 })
682 }
683}
684
685#[derive(Clone, Copy, Debug, Eq, PartialEq)]
725#[non_exhaustive]
726pub enum Gesture {
727 WrapRange(InlineKind),
728 ToggleInline(InlineKind),
729 SetBlock,
730 ToggleBlockContainer(BlockContainerKind),
731 InsertThematicBreak,
732 ToggleCodeBlock,
733 SetCodeLanguage,
734 ToggleTaskItem,
735 SetTaskChecked,
736 ToggleTaskChecked,
737 InsertLink,
738 InsertImage,
739 InsertFootnote,
740 InsertLiteral,
741 InsertLineBreak,
742 SplitBlock,
743 RenumberOrderedLists,
744 TableInsertRow,
745 TableDeleteRow,
746 TableInsertColumn,
747 TableDeleteColumn,
748 TableSetAlignment,
749 TableMoveRow,
750 TableMoveColumn,
751 SetMarkColor,
759}
760
761impl Gesture {
762 fn to_c(self) -> (c_int, c_int) {
767 match self {
768 Gesture::WrapRange(k) => (0, k.to_c()),
769 Gesture::ToggleInline(k) => (1, k.to_c()),
770 Gesture::SetBlock => (2, 0),
771 Gesture::ToggleBlockContainer(k) => (3, k.to_c()),
772 Gesture::InsertThematicBreak => (4, 0),
773 Gesture::ToggleCodeBlock => (5, 0),
774 Gesture::SetCodeLanguage => (6, 0),
775 Gesture::ToggleTaskItem => (7, 0),
776 Gesture::SetTaskChecked => (8, 0),
777 Gesture::ToggleTaskChecked => (9, 0),
778 Gesture::InsertLink => (10, 0),
779 Gesture::InsertImage => (11, 0),
780 Gesture::InsertFootnote => (12, 0),
781 Gesture::InsertLiteral => (13, 0),
782 Gesture::InsertLineBreak => (14, 0),
783 Gesture::SplitBlock => (15, 0),
784 Gesture::RenumberOrderedLists => (16, 0),
785 Gesture::TableInsertRow => (17, 0),
786 Gesture::TableDeleteRow => (18, 0),
787 Gesture::TableInsertColumn => (19, 0),
788 Gesture::TableDeleteColumn => (20, 0),
789 Gesture::TableSetAlignment => (21, 0),
790 Gesture::TableMoveRow => (22, 0),
791 Gesture::TableMoveColumn => (23, 0),
792 Gesture::SetMarkColor => (24, 0),
793 }
794 }
795}
796
797impl Format {
798 pub fn supports(self, gesture: Gesture) -> bool {
823 let (g, k) = gesture.to_c();
824 let mut supported: c_int = 0;
825 let status = unsafe {
826 ffi::twig_format_supports(ffi::TwigFormat::from(self) as c_int, g, k, &mut supported)
827 };
828 debug_assert!(
829 Error::from_status(status).is_ok(),
830 "twig_format_supports rejected a combination the Rust types make unrepresentable",
831 );
832 supported == 1
833 }
834
835 pub fn supports_with(self, extensions: MarkdownExtensions, gesture: Gesture) -> bool {
859 let (g, k) = gesture.to_c();
860 let mut supported: c_int = 0;
861 let status = unsafe {
862 ffi::twig_format_supports_ext(
863 ffi::TwigFormat::from(self) as c_int,
864 extensions.to_flags(),
865 g,
866 k,
867 &mut supported,
868 )
869 };
870 debug_assert!(
871 Error::from_status(status).is_ok(),
872 "twig_format_supports_ext rejected a combination the Rust types make unrepresentable",
873 );
874 supported == 1
875 }
876
877 pub fn is_authorable(self) -> bool {
888 let mut authorable: c_int = 0;
889 let status = unsafe {
890 ffi::twig_format_is_authorable(ffi::TwigFormat::from(self) as c_int, &mut authorable)
891 };
892 debug_assert!(Error::from_status(status).is_ok(), "unknown format code");
893 authorable == 1
894 }
895}
896
897#[derive(Clone, Copy, Debug, Eq, PartialEq)]
898pub struct Version {
899 pub major: u8,
900 pub minor: u8,
901 pub patch: u8,
902}
903
904pub fn version() -> Version {
905 let packed = unsafe { ffi::twig_version() };
906 Version {
907 major: (packed >> 16) as u8,
908 minor: (packed >> 8) as u8,
909 patch: packed as u8,
910 }
911}
912
913pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
919
920pub fn abi_version() -> u32 {
926 unsafe { ffi::twig_abi_version() }
927}
928
929pub fn version_string() -> &'static str {
930 let ptr = unsafe { ffi::twig_version_string() };
931 unsafe { std::ffi::CStr::from_ptr(ptr) }
932 .to_str()
933 .unwrap_or("")
934}
935
936#[derive(Debug)]
937pub struct Document {
938 raw: NonNull<ffi::TwigDocument>,
939}
940
941impl Document {
942 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
943 Self::parse_with(input, format, MarkdownExtensions::default())
944 }
945
946 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
947 Self::parse(input.as_bytes(), format)
948 }
949
950 pub fn parse_with(
956 input: &[u8],
957 format: Format,
958 extensions: MarkdownExtensions,
959 ) -> Result<Self, Error> {
960 let mut raw = std::ptr::null_mut();
961 let ffi_format: ffi::TwigFormat = format.into();
962 let status = unsafe {
963 ffi::twig_parse_ext(
964 input.as_ptr(),
965 input.len(),
966 ffi_format as i32,
967 extensions.to_flags(),
968 &mut raw,
969 )
970 };
971 Error::from_status(status)?;
972 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
973 Ok(Self { raw })
974 }
975
976 pub fn parse_str_with(
978 input: &str,
979 format: Format,
980 extensions: MarkdownExtensions,
981 ) -> Result<Self, Error> {
982 Self::parse_with(input.as_bytes(), format, extensions)
983 }
984
985 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
988 let raw = self.raw.as_ptr();
989 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
990 }
991
992 pub fn serialize_to(&mut self, target: Target) -> Result<Vec<u8>, Error> {
1003 let raw = self.raw.as_ptr();
1004 let ffi_target: ffi::TwigFormat = target.into();
1005 collect_bytes(|ptr, len| unsafe {
1006 ffi::twig_document_serialize(raw, ffi_target as i32, ptr, len)
1007 })
1008 }
1009
1010 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
1017 self.serialize_to(format.into())
1018 }
1019
1020 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
1023 let raw = self.raw.as_ptr();
1024 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
1025 }
1026
1027 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1036 let raw = self.raw.as_ptr();
1037 collect_matches(|ptr, len| unsafe {
1038 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
1039 })
1040 }
1041
1042 pub fn span(&mut self, node: NodeId) -> Result<Range<usize>, Error> {
1044 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1045 let status = unsafe { ffi::twig_document_node_span(self.raw.as_ptr(), node.0, &mut span) };
1046 Error::from_status(status)?;
1047 Ok(span.start..span.end)
1048 }
1049
1050 pub fn content_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
1053 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1054 let status =
1055 unsafe { ffi::twig_document_node_content_span(self.raw.as_ptr(), node.0, &mut span) };
1056 match status.0 {
1057 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1058 ffi::TwigStatus::NOT_FOUND => Ok(None),
1059 _ => Err(Error::from_status(status).unwrap_err()),
1060 }
1061 }
1062
1063 pub fn marker_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
1067 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1068 let status =
1069 unsafe { ffi::twig_document_node_marker_span(self.raw.as_ptr(), node.0, &mut span) };
1070 match status.0 {
1071 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1072 ffi::TwigStatus::NOT_FOUND => Ok(None),
1073 _ => Err(Error::from_status(status).unwrap_err()),
1074 }
1075 }
1076
1077 pub fn attrs_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
1092 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1093 let status =
1094 unsafe { ffi::twig_document_attrs_span(self.raw.as_ptr(), node.0, &mut span) };
1095 match status.0 {
1096 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1097 ffi::TwigStatus::NOT_FOUND => Ok(None),
1098 _ => Err(Error::from_status(status).unwrap_err()),
1099 }
1100 }
1101
1102 pub fn line_prefix(&mut self, offset: usize) -> Result<Option<Range<usize>>, Error> {
1119 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1120 let status =
1121 unsafe { ffi::twig_document_line_prefix(self.raw.as_ptr(), offset, &mut span) };
1122 match status.0 {
1123 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1124 ffi::TwigStatus::NOT_FOUND => Ok(None),
1125 _ => Err(Error::from_status(status).unwrap_err()),
1126 }
1127 }
1128
1129 pub fn continuation_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1155 self.prefix_via(offset, ffi::twig_document_continuation_prefix)
1156 }
1157
1158 pub fn blank_line_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1170 self.prefix_via(offset, ffi::twig_document_blank_line_prefix)
1171 }
1172
1173 fn prefix_via(
1175 &mut self,
1176 offset: usize,
1177 f: unsafe extern "C" fn(
1178 *mut ffi::TwigDocument,
1179 usize,
1180 *mut *const u8,
1181 *mut usize,
1182 *mut usize,
1183 ) -> ffi::TwigStatus,
1184 ) -> Result<LinePrefix, Error> {
1185 let mut ptr: *const u8 = std::ptr::null();
1186 let mut len = 0usize;
1187 let mut columns = 0usize;
1188 let status = unsafe { f(self.raw.as_ptr(), offset, &mut ptr, &mut len, &mut columns) };
1189 Error::from_status(status)?;
1190 let text = if ptr.is_null() || len == 0 {
1191 String::new()
1192 } else {
1193 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1194 String::from_utf8(bytes.to_vec()).map_err(|_| Error::Internal)?
1195 };
1196 Ok(LinePrefix { text, columns })
1197 }
1198
1199 pub fn cell_extent(&mut self, node: NodeId) -> Result<Option<(u32, u32)>, Error> {
1211 let raw = self.raw.as_ptr();
1212 let mut colspan: u32 = 0;
1213 let status = unsafe { ffi::twig_document_cell_colspan(raw, node.0, &mut colspan) };
1214 match status.0 {
1215 ffi::TwigStatus::OK => {}
1216 ffi::TwigStatus::NOT_FOUND => return Ok(None),
1217 _ => return Err(Error::from_status(status).unwrap_err()),
1218 }
1219 let mut rowspan: u32 = 0;
1220 Error::from_status(unsafe { ffi::twig_document_cell_rowspan(raw, node.0, &mut rowspan) })?;
1221 Ok(Some((colspan, rowspan)))
1222 }
1223
1224 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1229 let raw = self.raw.as_ptr();
1230 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_nodes(raw, ptr, len) })
1231 }
1232
1233 pub fn definitions(&mut self) -> Result<Vec<QueryMatch>, Error> {
1250 let raw = self.raw.as_ptr();
1251 collect_matches(|ptr, len| unsafe { ffi::twig_document_definitions(raw, ptr, len) })
1252 }
1253
1254 pub fn diagnostics(&mut self, target: Target) -> Result<Vec<Warning>, Error> {
1274 let raw = self.raw.as_ptr();
1275 let code = ffi::TwigFormat::from(target) as c_int;
1276 let mut ptr: *const ffi::TwigWarning = std::ptr::null();
1277 let mut len = 0usize;
1278 let status = unsafe { ffi::twig_document_diagnostics(raw, code, &mut ptr, &mut len) };
1279 Error::from_status(status)?;
1280 if len == 0 || ptr.is_null() {
1281 return Ok(Vec::new());
1282 }
1283 let raw_warnings = unsafe { std::slice::from_raw_parts(ptr, len) };
1284 Ok(raw_warnings
1285 .iter()
1286 .map(|w| Warning {
1287 fidelity: Fidelity::from_c(w.fidelity),
1288 path: borrowed_bytes(w.path_ptr, w.path_len).unwrap_or_default(),
1289 kind: Kind::from(borrowed_cstr(w.kind).unwrap_or_default().as_str()),
1290 })
1291 .collect())
1292 }
1293
1294 pub fn children(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1300 let raw = self.raw.as_ptr();
1301 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1302 collect_matches(|ptr, len| unsafe { ffi::twig_document_children(raw, id, ptr, len) })
1303 }
1304
1305 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1311 let raw = self.raw.as_ptr();
1312 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_subtree(raw, node.0, ptr, len) })
1313 }
1314
1315 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1320 let mut m = empty_ffi_match();
1321 let status = unsafe { ffi::twig_document_node_at(self.raw.as_ptr(), offset, &mut m) };
1322 match status.0 {
1323 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1324 ffi::TwigStatus::NOT_FOUND => Ok(None),
1325 _ => Err(Error::from_status(status).unwrap_err()),
1326 }
1327 }
1328
1329 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1333 let raw = self.raw.as_ptr();
1334 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1335 let mut len = 0usize;
1336 let status = unsafe { ffi::twig_document_nodes_at(raw, offset, &mut ptr, &mut len) };
1337 match status.0 {
1338 ffi::TwigStatus::OK => {}
1339 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1340 _ => return Err(Error::from_status(status).unwrap_err()),
1341 }
1342 if len == 0 || ptr.is_null() {
1343 return Ok(Vec::new());
1344 }
1345 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1346 raw_matches.iter().map(query_match_from_ffi).collect()
1347 }
1348
1349 pub fn node_at_caret(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1372 let mut m = empty_ffi_match();
1373 let status = unsafe { ffi::twig_document_node_at_caret(self.raw.as_ptr(), offset, &mut m) };
1374 match status.0 {
1375 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1376 ffi::TwigStatus::NOT_FOUND => Ok(None),
1377 _ => Err(Error::from_status(status).unwrap_err()),
1378 }
1379 }
1380
1381 pub fn ancestors_at_caret(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1385 let raw = self.raw.as_ptr();
1386 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1387 let mut len = 0usize;
1388 let status = unsafe { ffi::twig_document_nodes_at_caret(raw, offset, &mut ptr, &mut len) };
1389 match status.0 {
1390 ffi::TwigStatus::OK => {}
1391 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1392 _ => return Err(Error::from_status(status).unwrap_err()),
1393 }
1394 if len == 0 || ptr.is_null() {
1395 return Ok(Vec::new());
1396 }
1397 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1398 raw_matches.iter().map(query_match_from_ffi).collect()
1399 }
1400}
1401
1402#[derive(Debug)]
1413pub struct DocumentView<'a> {
1414 doc: Document,
1415 _editor: PhantomData<&'a mut Editor>,
1416}
1417
1418impl std::ops::Deref for DocumentView<'_> {
1419 type Target = Document;
1420
1421 fn deref(&self) -> &Document {
1422 &self.doc
1423 }
1424}
1425
1426impl std::ops::DerefMut for DocumentView<'_> {
1427 fn deref_mut(&mut self) -> &mut Document {
1428 &mut self.doc
1429 }
1430}
1431
1432impl Drop for Document {
1433 fn drop(&mut self) {
1434 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
1435 }
1436}
1437
1438#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1444pub struct MarkdownExtensions {
1445 pub directives: bool,
1447 pub math: bool,
1449 pub html_elements: bool,
1454 pub highlight: bool,
1457 pub highlight_colors: bool,
1463}
1464
1465impl MarkdownExtensions {
1466 fn to_flags(self) -> u32 {
1467 let mut flags = 0;
1468 if self.directives {
1469 flags |= ffi::TWIG_MD_DIRECTIVES;
1470 }
1471 if self.math {
1472 flags |= ffi::TWIG_MD_MATH;
1473 }
1474 if self.html_elements {
1475 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
1476 }
1477 if self.highlight {
1478 flags |= ffi::TWIG_MD_HIGHLIGHT;
1479 }
1480 if self.highlight_colors {
1481 flags |= ffi::TWIG_MD_HIGHLIGHT_COLORS;
1482 }
1483 flags
1484 }
1485}
1486
1487#[derive(Debug)]
1493pub struct Editor {
1494 raw: NonNull<ffi::TwigEditor>,
1495}
1496
1497impl Editor {
1498 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
1501 let mut raw = std::ptr::null_mut();
1502 let ffi_format: ffi::TwigFormat = format.into();
1503 let status = unsafe {
1504 ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw)
1505 };
1506 Error::from_status(status)?;
1507 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1508 Ok(Self { raw })
1509 }
1510
1511 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
1512 Self::new(input.as_bytes(), format)
1513 }
1514
1515 pub fn new_ext(
1529 input: &[u8],
1530 format: Format,
1531 extensions: MarkdownExtensions,
1532 ) -> Result<Self, Error> {
1533 let mut raw = std::ptr::null_mut();
1534 let ffi_format: ffi::TwigFormat = format.into();
1535 let status = unsafe {
1536 ffi::twig_editor_create_ext(
1537 input.as_ptr(),
1538 input.len(),
1539 ffi_format as i32,
1540 extensions.to_flags(),
1541 &mut raw,
1542 )
1543 };
1544 Error::from_status(status)?;
1545 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1546 Ok(Self { raw })
1547 }
1548
1549 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1551 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1552 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
1553 })
1554 }
1555
1556 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1559 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1560 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
1561 })
1562 }
1563
1564 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1566 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1567 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
1568 })
1569 }
1570
1571 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1573 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1574 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
1575 })
1576 }
1577
1578 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
1581 let status = unsafe {
1582 ffi::twig_editor_insert_child(
1583 self.raw.as_ptr(),
1584 locator.as_ptr(),
1585 locator.len(),
1586 index,
1587 text.as_ptr(),
1588 text.len(),
1589 )
1590 };
1591 Error::from_status(status)
1592 }
1593
1594 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
1597 let status =
1598 unsafe { ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1599 Error::from_status(status)
1600 }
1601
1602 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
1605 let status = unsafe {
1606 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
1607 };
1608 Error::from_status(status)
1609 }
1610
1611 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
1615 let status =
1616 unsafe { ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1617 Error::from_status(status)
1618 }
1619
1620 pub fn filter(
1625 &mut self,
1626 drop: &str,
1627 keep: Option<&str>,
1628 unwrap_kept: bool,
1629 ) -> Result<(), Error> {
1630 let (keep_ptr, keep_len) = match keep {
1631 Some(k) => (k.as_ptr(), k.len()),
1632 None => (std::ptr::null(), 0),
1633 };
1634 let status = unsafe {
1635 ffi::twig_editor_filter(
1636 self.raw.as_ptr(),
1637 drop.as_ptr(),
1638 drop.len(),
1639 keep_ptr,
1640 keep_len,
1641 unwrap_kept as i32,
1642 )
1643 };
1644 Error::from_status(status)
1645 }
1646
1647 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
1649 let raw = self.raw.as_ptr();
1650 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
1651 }
1652
1653 pub fn source_str(&mut self) -> Result<String, Error> {
1655 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
1656 }
1657
1658 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
1661 let raw = self.raw.as_ptr();
1662 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
1663 }
1664
1665 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1668 let raw = self.raw.as_ptr();
1669 collect_matches(|ptr, len| unsafe {
1670 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
1671 })
1672 }
1673
1674 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
1684 let mut change = ffi::TwigChange {
1685 old_span: ffi::TwigSpan { start: 0, end: 0 },
1686 new_span: ffi::TwigSpan { start: 0, end: 0 },
1687 };
1688 let status = unsafe {
1689 ffi::twig_editor_edit_range(
1690 self.raw.as_ptr(),
1691 start,
1692 end,
1693 text.as_ptr(),
1694 text.len(),
1695 &mut change,
1696 )
1697 };
1698 Error::from_status(status)?;
1699 Ok(Change::from_ffi(change))
1700 }
1701
1702 pub fn last_change(&mut self) -> Option<Change> {
1708 let mut change = ffi::TwigChange {
1709 old_span: ffi::TwigSpan { start: 0, end: 0 },
1710 new_span: ffi::TwigSpan { start: 0, end: 0 },
1711 };
1712 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
1713 match status.0 {
1714 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
1715 _ => None,
1716 }
1717 }
1718
1719 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
1724 let mut change = ffi::TwigChange {
1725 old_span: ffi::TwigSpan { start: 0, end: 0 },
1726 new_span: ffi::TwigSpan { start: 0, end: 0 },
1727 };
1728 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
1729 if status.0 == ffi::TwigStatus::NOT_FOUND {
1730 return Ok(None);
1731 }
1732 Error::from_status(status)?;
1733 Ok(Some(Change::from_ffi(change)))
1734 }
1735
1736 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
1740 let mut change = ffi::TwigChange {
1741 old_span: ffi::TwigSpan { start: 0, end: 0 },
1742 new_span: ffi::TwigSpan { start: 0, end: 0 },
1743 };
1744 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
1745 if status.0 == ffi::TwigStatus::NOT_FOUND {
1746 return Ok(None);
1747 }
1748 Error::from_status(status)?;
1749 Ok(Some(Change::from_ffi(change)))
1750 }
1751
1752 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
1757 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
1758 Error::from_status(status)
1759 }
1760
1761 pub fn revision(&mut self) -> u64 {
1767 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
1768 }
1769
1770 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
1791 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1792 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
1793 match status.0 {
1794 ffi::TwigStatus::OK => Some(span.start..span.end),
1795 _ => None,
1796 }
1797 }
1798
1799 pub fn clear_dirty(&mut self) {
1804 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
1805 }
1806
1807 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
1815 let status = unsafe {
1816 ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len())
1817 };
1818 Error::from_status(status)
1819 }
1820
1821 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
1826 let raw = self.raw.as_ptr();
1827 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
1828 }
1829
1830 pub fn document(&mut self) -> Result<DocumentView<'_>, Error> {
1839 let mut raw = std::ptr::null_mut();
1840 let status = unsafe { ffi::twig_editor_document(self.raw.as_ptr(), &mut raw) };
1841 Error::from_status(status)?;
1842 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1843 Ok(DocumentView {
1844 doc: Document { raw },
1845 _editor: PhantomData,
1846 })
1847 }
1848
1849 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1854 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1855 let mut len = 0usize;
1856 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
1857 Error::from_status(status)?;
1858 if len == 0 {
1859 return Ok(Vec::new());
1860 }
1861 if ptr.is_null() {
1862 return Err(Error::Internal);
1863 }
1864 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1865 raw.iter().map(flat_node_from_ffi).collect()
1866 }
1867
1868 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1875 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1876 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1877 let mut len = 0usize;
1878 let status =
1879 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
1880 Error::from_status(status)?;
1881 if len == 0 || ptr.is_null() {
1882 return Ok(Vec::new());
1883 }
1884 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1885 raw.iter().map(query_match_from_ffi).collect()
1886 }
1887
1888 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1896 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1897 let mut len = 0usize;
1898 let status =
1899 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
1900 Error::from_status(status)?;
1901 if len == 0 || ptr.is_null() {
1902 return Ok(Vec::new());
1903 }
1904 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1905 raw.iter().map(flat_node_from_ffi).collect()
1906 }
1907
1908 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1913 let mut m = ffi::TwigQueryMatch {
1914 node_id: 0,
1915 span: ffi::TwigSpan { start: 0, end: 0 },
1916 content_span: ffi::TwigSpan { start: 0, end: 0 },
1917 has_content_span: 0,
1918 kind: std::ptr::null(),
1919 };
1920 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
1921 match status.0 {
1922 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1923 ffi::TwigStatus::NOT_FOUND => Ok(None),
1924 _ => Err(Error::from_status(status).unwrap_err()),
1925 }
1926 }
1927
1928 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1932 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1933 let mut len = 0usize;
1934 let status =
1935 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
1936 match status.0 {
1937 ffi::TwigStatus::OK => {}
1938 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1939 _ => return Err(Error::from_status(status).unwrap_err()),
1940 }
1941 if len == 0 || ptr.is_null() {
1942 return Ok(Vec::new());
1943 }
1944 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1945 raw.iter().map(query_match_from_ffi).collect()
1946 }
1947
1948 pub fn wrap_range(
1956 &mut self,
1957 start: usize,
1958 end: usize,
1959 kind: InlineKind,
1960 ) -> Result<Change, Error> {
1961 self.change_op(|ed, out| unsafe {
1962 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
1963 })
1964 }
1965
1966 pub fn toggle_inline(
1971 &mut self,
1972 start: usize,
1973 end: usize,
1974 kind: InlineKind,
1975 ) -> Result<Change, Error> {
1976 self.change_op(|ed, out| unsafe {
1977 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
1978 })
1979 }
1980
1981 pub fn set_mark_color(
2019 &mut self,
2020 offset: usize,
2021 color: Option<MarkColor>,
2022 ) -> Result<Change, Error> {
2023 let name = color.map(MarkColor::as_str);
2024 let (ptr, len, has) = opt_str(name);
2025 self.change_op(|ed, out| unsafe {
2026 ffi::twig_editor_set_mark_color(ed, offset, ptr, len, has, out)
2027 })
2028 }
2029
2030 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
2049 let (block_kind, level) = kind.to_c();
2050 self.change_op(|ed, out| unsafe {
2051 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
2052 })
2053 }
2054
2055 pub fn toggle_block_container(
2078 &mut self,
2079 start: usize,
2080 end: usize,
2081 kind: BlockContainerKind,
2082 ) -> Result<Change, Error> {
2083 self.change_op(|ed, out| unsafe {
2084 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
2085 })
2086 }
2087
2088 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
2107 self.change_op(|ed, out| unsafe {
2108 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
2109 })?;
2110 Ok(())
2111 }
2112
2113 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
2122 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
2123 }
2124
2125 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
2128 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
2129 }
2130
2131 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
2133 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
2134 }
2135
2136 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
2138 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
2139 }
2140
2141 pub fn table_set_alignment(
2143 &mut self,
2144 offset: usize,
2145 alignment: Alignment,
2146 ) -> Result<(), Error> {
2147 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
2148 }
2149
2150 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
2152 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
2153 }
2154
2155 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
2157 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
2158 }
2159
2160 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
2161 self.change_op(|ed, out| unsafe { ffi::twig_editor_table_edit(ed, offset, op, arg, out) })?;
2162 Ok(())
2163 }
2164
2165 pub fn insert_link(
2210 &mut self,
2211 start: usize,
2212 end: usize,
2213 destination: &str,
2214 ) -> Result<Change, Error> {
2215 self.change_op(|ed, out| unsafe {
2216 ffi::twig_editor_insert_link(
2217 ed,
2218 start,
2219 end,
2220 destination.as_ptr(),
2221 destination.len(),
2222 out,
2223 )
2224 })
2225 }
2226
2227 pub fn insert_image(
2248 &mut self,
2249 start: usize,
2250 end: usize,
2251 destination: &str,
2252 ) -> Result<Change, Error> {
2253 self.change_op(|ed, out| unsafe {
2254 ffi::twig_editor_insert_image(
2255 ed,
2256 start,
2257 end,
2258 destination.as_ptr(),
2259 destination.len(),
2260 out,
2261 )
2262 })
2263 }
2264
2265 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
2286 self.change_op(|ed, out| unsafe {
2287 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
2288 })
2289 }
2290
2291 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
2305 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
2306 }
2307
2308 pub fn insert_thematic_break(&mut self, offset: usize) -> Result<Change, Error> {
2327 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_thematic_break(ed, offset, out) })
2328 }
2329
2330 pub fn split_block(&mut self, offset: usize) -> Result<Change, Error> {
2378 self.change_op(|ed, out| unsafe { ffi::twig_editor_split_block(ed, offset, out) })
2379 }
2380
2381 pub fn toggle_code_block(
2413 &mut self,
2414 start: usize,
2415 end: usize,
2416 language: Option<&str>,
2417 ) -> Result<Change, Error> {
2418 let (ptr, len, has) = opt_str(language);
2419 self.change_op(|ed, out| unsafe {
2420 ffi::twig_editor_toggle_code_block(ed, start, end, ptr, len, has, out)
2421 })
2422 }
2423
2424 pub fn set_code_language(
2434 &mut self,
2435 offset: usize,
2436 language: Option<&str>,
2437 ) -> Result<Change, Error> {
2438 let (ptr, len, has) = opt_str(language);
2439 self.change_op(|ed, out| unsafe {
2440 ffi::twig_editor_set_code_language(ed, offset, ptr, len, has, out)
2441 })
2442 }
2443
2444 pub fn toggle_task_item(&mut self, offset: usize) -> Result<Change, Error> {
2455 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_item(ed, offset, out) })
2456 }
2457
2458 pub fn set_task_checked(&mut self, offset: usize, checked: bool) -> Result<(), Error> {
2472 self.change_op(|ed, out| unsafe {
2473 ffi::twig_editor_set_task_checked(ed, offset, checked as c_int, out)
2474 })?;
2475 Ok(())
2476 }
2477
2478 pub fn toggle_task_checked(&mut self, offset: usize) -> Result<Change, Error> {
2483 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_checked(ed, offset, out) })
2484 }
2485
2486 pub fn insert_footnote(&mut self, offset: usize, label: &str) -> Result<Change, Error> {
2505 self.change_op(|ed, out| unsafe {
2506 ffi::twig_editor_insert_footnote(ed, offset, label.as_ptr(), label.len(), out)
2507 })
2508 }
2509
2510 fn change_op(
2513 &mut self,
2514 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
2515 ) -> Result<Change, Error> {
2516 let mut change = ffi::TwigChange {
2517 old_span: ffi::TwigSpan { start: 0, end: 0 },
2518 new_span: ffi::TwigSpan { start: 0, end: 0 },
2519 };
2520 let status = op(self.raw.as_ptr(), &mut change);
2521 Error::from_status(status)?;
2522 Ok(Change::from_ffi(change))
2523 }
2524
2525 fn apply(
2527 &mut self,
2528 locator: &str,
2529 text: &str,
2530 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
2531 ) -> Result<(), Error> {
2532 let status = op(
2533 self.raw.as_ptr(),
2534 locator.as_ptr(),
2535 locator.len(),
2536 text.as_ptr(),
2537 text.len(),
2538 );
2539 Error::from_status(status)
2540 }
2541}
2542
2543impl Drop for Editor {
2544 fn drop(&mut self) {
2545 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
2546 }
2547}
2548
2549fn collect_bytes(
2554 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
2555) -> Result<Vec<u8>, Error> {
2556 let mut ptr = std::ptr::null();
2557 let mut len = 0usize;
2558 let status = call(&mut ptr, &mut len);
2559 Error::from_status(status)?;
2560 if len == 0 {
2561 return Ok(Vec::new());
2562 }
2563 if ptr.is_null() {
2564 return Err(Error::Internal);
2565 }
2566 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2567 Ok(bytes.to_vec())
2568}
2569
2570fn collect_matches(
2573 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
2574) -> Result<Vec<QueryMatch>, Error> {
2575 let mut ptr = std::ptr::null();
2576 let mut len = 0usize;
2577 let status = call(&mut ptr, &mut len);
2578 Error::from_status(status)?;
2579 if len == 0 {
2580 return Ok(Vec::new());
2581 }
2582 if ptr.is_null() {
2583 return Err(Error::Internal);
2584 }
2585 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
2586 matches.iter().map(query_match_from_ffi).collect()
2587}
2588
2589fn collect_flat_nodes(
2592 call: impl FnOnce(*mut *const ffi::TwigFlatNode, *mut usize) -> ffi::TwigStatus,
2593) -> Result<Vec<FlatNode>, Error> {
2594 let mut ptr = std::ptr::null();
2595 let mut len = 0usize;
2596 let status = call(&mut ptr, &mut len);
2597 Error::from_status(status)?;
2598 if len == 0 {
2599 return Ok(Vec::new());
2600 }
2601 if ptr.is_null() {
2602 return Err(Error::Internal);
2603 }
2604 let nodes = unsafe { std::slice::from_raw_parts(ptr, len) };
2605 nodes.iter().map(flat_node_from_ffi).collect()
2606}
2607
2608fn empty_ffi_match() -> ffi::TwigQueryMatch {
2610 ffi::TwigQueryMatch {
2611 node_id: 0,
2612 span: ffi::TwigSpan { start: 0, end: 0 },
2613 content_span: ffi::TwigSpan { start: 0, end: 0 },
2614 has_content_span: 0,
2615 kind: std::ptr::null(),
2616 }
2617}
2618
2619fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
2622 Ok(QueryMatch {
2623 node_id: m.node_id,
2624 span: m.span.start..m.span.end,
2625 content_span: if m.has_content_span != 0 {
2626 Some(m.content_span.start..m.content_span.end)
2627 } else {
2628 None
2629 },
2630 kind: Kind::from(borrowed_cstr(m.kind)?.as_str()),
2631 })
2632}
2633
2634fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
2636 let node_id = |v: u32| {
2637 if v == ffi::TWIG_NO_NODE {
2638 None
2639 } else {
2640 Some(NodeId(v))
2641 }
2642 };
2643 Ok(FlatNode {
2644 id: NodeId(n.id),
2645 parent: node_id(n.parent),
2646 first_child: node_id(n.first_child),
2647 next_sibling: node_id(n.next_sibling),
2648 span: n.span.start..n.span.end,
2649 content_span: if n.has_content_span != 0 {
2650 Some(n.content_span.start..n.content_span.end)
2651 } else {
2652 None
2653 },
2654 level: if n.level != 0 { Some(n.level) } else { None },
2655 kind: Kind::from(borrowed_cstr(n.kind)?.as_str()),
2656 text: borrowed_bytes(n.text_ptr, n.text_len),
2657 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
2658 head: match n.head {
2659 ffi::TWIG_HEAD_NONE => None,
2660 v => Some(v != 0),
2661 },
2662 alignment: Alignment::from_c(n.alignment),
2663 name: borrowed_bytes(n.name_ptr, n.name_len),
2664 directive_form: DirectiveForm::from_c(n.directive_form),
2665 origin: ContainerOrigin::from_c(n.container_origin),
2666 marker_span: if n.has_marker_span != 0 {
2667 Some(n.marker_span.start..n.marker_span.end)
2668 } else {
2669 None
2670 },
2671 checked: match n.checked {
2672 ffi::TWIG_TASK_CHECKED_NONE => None,
2673 v => Some(v != 0),
2674 },
2675 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
2676 })
2677}
2678
2679fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
2683 if ptr.is_null() || len == 0 {
2684 return Vec::new();
2685 }
2686 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
2687 kvs.iter()
2688 .map(|kv| {
2689 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
2690 (key, borrowed_bytes(kv.value, kv.value_len))
2691 })
2692 .collect()
2693}
2694
2695fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
2697 if ptr.is_null() {
2698 return Err(Error::Internal);
2699 }
2700 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
2701 .to_str()
2702 .map_err(|_| Error::Internal)?
2703 .to_owned())
2704}
2705
2706fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
2710 if ptr.is_null() {
2711 return None;
2712 }
2713 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2714 Some(String::from_utf8_lossy(bytes).into_owned())
2715}
2716
2717#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
2721pub struct NodeId(pub u32);
2722
2723#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2726pub enum VoidKind {
2727 Doc,
2728 Para,
2729 ThematicBreak,
2730 Section,
2731 Div,
2732 BlockQuote,
2733 DefinitionList,
2734 Table,
2735 ListItem,
2736 DefinitionListItem,
2737 Term,
2738 Definition,
2739 Caption,
2740 SoftBreak,
2741 HardBreak,
2742 NonBreakingSpace,
2743 Emph,
2744 Strong,
2745 Span,
2746 Mark,
2747 Superscript,
2748 Subscript,
2749 Insert,
2750 Delete,
2751 DoubleQuoted,
2752 SingleQuoted,
2753}
2754
2755impl VoidKind {
2756 fn to_c(self) -> c_int {
2757 match self {
2759 VoidKind::Doc => 0,
2760 VoidKind::Para => 1,
2761 VoidKind::ThematicBreak => 3,
2762 VoidKind::Section => 4,
2763 VoidKind::Div => 5,
2764 VoidKind::BlockQuote => 9,
2765 VoidKind::DefinitionList => 13,
2766 VoidKind::Table => 14,
2767 VoidKind::ListItem => 15,
2768 VoidKind::DefinitionListItem => 17,
2769 VoidKind::Term => 18,
2770 VoidKind::Definition => 19,
2771 VoidKind::Caption => 22,
2772 VoidKind::SoftBreak => 26,
2773 VoidKind::HardBreak => 27,
2774 VoidKind::NonBreakingSpace => 28,
2775 VoidKind::Emph => 38,
2776 VoidKind::Strong => 39,
2777 VoidKind::Span => 42,
2778 VoidKind::Mark => 43,
2779 VoidKind::Superscript => 44,
2780 VoidKind::Subscript => 45,
2781 VoidKind::Insert => 46,
2782 VoidKind::Delete => 47,
2783 VoidKind::DoubleQuoted => 48,
2784 VoidKind::SingleQuoted => 49,
2785 }
2786 }
2787}
2788
2789#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2791pub enum TextKind {
2792 Str,
2793 Symb,
2794 Verbatim,
2795 InlineMath,
2796 DisplayMath,
2797 Url,
2798 Email,
2799 FootnoteReference,
2800 CitationReference,
2803 SubstitutionReference,
2805 Comment,
2806 Doctype,
2807 Cdata,
2808}
2809
2810impl TextKind {
2811 fn to_c(self) -> c_int {
2812 match self {
2813 TextKind::Str => 25,
2814 TextKind::Symb => 29,
2815 TextKind::Verbatim => 30,
2816 TextKind::InlineMath => 32,
2817 TextKind::DisplayMath => 33,
2818 TextKind::Url => 34,
2819 TextKind::Email => 35,
2820 TextKind::FootnoteReference => 36,
2821 TextKind::CitationReference => 58,
2822 TextKind::SubstitutionReference => 59,
2823 TextKind::Comment => 52,
2824 TextKind::Doctype => 53,
2825 TextKind::Cdata => 55,
2826 }
2827 }
2828}
2829
2830#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2832pub enum BulletStyle {
2833 Dash,
2834 Plus,
2835 Star,
2836}
2837
2838impl BulletStyle {
2839 fn to_c(self) -> c_int {
2840 match self {
2841 BulletStyle::Dash => 0,
2842 BulletStyle::Plus => 1,
2843 BulletStyle::Star => 2,
2844 }
2845 }
2846}
2847
2848#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2850pub enum OrderedNumbering {
2851 Decimal,
2852 LowerAlpha,
2853 UpperAlpha,
2854 LowerRoman,
2855 UpperRoman,
2856}
2857
2858impl OrderedNumbering {
2859 fn to_c(self) -> c_int {
2860 match self {
2861 OrderedNumbering::Decimal => 0,
2862 OrderedNumbering::LowerAlpha => 1,
2863 OrderedNumbering::UpperAlpha => 2,
2864 OrderedNumbering::LowerRoman => 3,
2865 OrderedNumbering::UpperRoman => 4,
2866 }
2867 }
2868}
2869
2870#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2872pub enum OrderedDelim {
2873 Period,
2874 ParenAfter,
2875 ParenBoth,
2876}
2877
2878impl OrderedDelim {
2879 fn to_c(self) -> c_int {
2880 match self {
2881 OrderedDelim::Period => 0,
2882 OrderedDelim::ParenAfter => 1,
2883 OrderedDelim::ParenBoth => 2,
2884 }
2885 }
2886}
2887
2888#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2891pub enum Alignment {
2892 Default,
2893 Left,
2894 Right,
2895 Center,
2896}
2897
2898impl Alignment {
2899 fn to_c(self) -> c_int {
2900 match self {
2901 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
2902 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
2903 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
2904 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
2905 }
2906 }
2907
2908 fn from_c(v: c_int) -> Option<Self> {
2911 match v {
2912 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
2913 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
2914 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
2915 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
2916 _ => None,
2917 }
2918 }
2919}
2920
2921#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2923pub enum SmartPunctuation {
2924 LeftSingleQuote,
2925 RightSingleQuote,
2926 LeftDoubleQuote,
2927 RightDoubleQuote,
2928 Ellipses,
2929 EmDash,
2930 EnDash,
2931}
2932
2933impl SmartPunctuation {
2934 fn to_c(self) -> c_int {
2935 match self {
2936 SmartPunctuation::LeftSingleQuote => 0,
2937 SmartPunctuation::RightSingleQuote => 1,
2938 SmartPunctuation::LeftDoubleQuote => 2,
2939 SmartPunctuation::RightDoubleQuote => 3,
2940 SmartPunctuation::Ellipses => 4,
2941 SmartPunctuation::EmDash => 5,
2942 SmartPunctuation::EnDash => 6,
2943 }
2944 }
2945}
2946
2947#[derive(Clone, Debug, Eq, PartialEq)]
2962pub struct Warning {
2963 pub fidelity: Fidelity,
2964 pub path: String,
2971 pub kind: Kind,
2974}
2975
2976#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2978#[non_exhaustive]
2979pub enum Fidelity {
2980 Degraded,
2983 Dropped,
2985}
2986
2987impl Fidelity {
2988 fn from_c(v: c_int) -> Self {
2992 match v {
2993 ffi::TWIG_FIDELITY_DROPPED => Fidelity::Dropped,
2994 _ => Fidelity::Degraded,
2995 }
2996 }
2997}
2998
2999#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3000#[non_exhaustive]
3001pub enum ContainerOrigin {
3002 Element,
3004 Directive,
3008}
3009
3010impl ContainerOrigin {
3011 fn from_c(v: c_int) -> Option<Self> {
3014 match v {
3015 ffi::TWIG_CONTAINER_ORIGIN_ELEMENT => Some(ContainerOrigin::Element),
3016 ffi::TWIG_CONTAINER_ORIGIN_DIRECTIVE => Some(ContainerOrigin::Directive),
3017 _ => None,
3018 }
3019 }
3020}
3021
3022#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3024pub enum DirectiveForm {
3025 Text,
3026 Leaf,
3027 Container,
3028}
3029
3030impl DirectiveForm {
3031 fn to_c(self) -> c_int {
3032 match self {
3033 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
3034 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
3035 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
3036 }
3037 }
3038
3039 fn from_c(v: c_int) -> Option<Self> {
3043 match v {
3044 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
3045 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
3046 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
3047 _ => None,
3048 }
3049 }
3050}
3051
3052fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
3056 match s {
3057 Some(x) => (x.as_ptr(), x.len(), 1),
3058 None => (std::ptr::null(), 0, 0),
3059 }
3060}
3061
3062#[derive(Debug)]
3069pub struct Builder {
3070 raw: NonNull<ffi::TwigBuilder>,
3071}
3072
3073impl Builder {
3074 pub fn new() -> Result<Self, Error> {
3076 let mut raw = std::ptr::null_mut();
3077 let status = unsafe { ffi::twig_builder_create(&mut raw) };
3078 Error::from_status(status)?;
3079 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
3080 Ok(Self { raw })
3081 }
3082
3083 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
3086 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
3087 }
3088
3089 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
3091 self.emit(|b, out| unsafe {
3092 ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out)
3093 })
3094 }
3095
3096 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
3098 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
3099 }
3100
3101 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
3103 let (lp, ll, has) = opt_str(lang);
3104 self.emit(|b, out| unsafe {
3105 ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out)
3106 })
3107 }
3108
3109 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
3111 self.emit(|b, out| unsafe {
3112 ffi::twig_builder_add_raw_block(
3113 b,
3114 format.as_ptr(),
3115 format.len(),
3116 text.as_ptr(),
3117 text.len(),
3118 out,
3119 )
3120 })
3121 }
3122
3123 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
3125 self.emit(|b, out| unsafe {
3126 ffi::twig_builder_add_metadata(
3127 b,
3128 lang.as_ptr(),
3129 lang.len(),
3130 text.as_ptr(),
3131 text.len(),
3132 out,
3133 )
3134 })
3135 }
3136
3137 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
3139 self.emit(|b, out| unsafe {
3140 ffi::twig_builder_add_raw_inline(
3141 b,
3142 format.as_ptr(),
3143 format.len(),
3144 text.as_ptr(),
3145 text.len(),
3146 out,
3147 )
3148 })
3149 }
3150
3151 pub fn add_smart_punctuation(
3156 &mut self,
3157 kind: SmartPunctuation,
3158 text: &str,
3159 ) -> Result<NodeId, Error> {
3160 self.emit(|b, out| unsafe {
3161 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
3162 })
3163 }
3164
3165 pub fn add_link(
3168 &mut self,
3169 destination: Option<&str>,
3170 reference: Option<&str>,
3171 ) -> Result<NodeId, Error> {
3172 let (dp, dl, hd) = opt_str(destination);
3173 let (rp, rl, hr) = opt_str(reference);
3174 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
3175 }
3176
3177 pub fn add_image(
3179 &mut self,
3180 destination: Option<&str>,
3181 reference: Option<&str>,
3182 ) -> Result<NodeId, Error> {
3183 let (dp, dl, hd) = opt_str(destination);
3184 let (rp, rl, hr) = opt_str(reference);
3185 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
3186 }
3187
3188 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
3190 self.emit(|b, out| unsafe {
3191 ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out)
3192 })
3193 }
3194
3195 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
3197 self.emit(|b, out| unsafe {
3198 ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out)
3199 })
3200 }
3201
3202 pub fn add_processing_instruction(
3204 &mut self,
3205 target: &str,
3206 data: &str,
3207 ) -> Result<NodeId, Error> {
3208 self.emit(|b, out| unsafe {
3209 ffi::twig_builder_add_processing_instruction(
3210 b,
3211 target.as_ptr(),
3212 target.len(),
3213 data.as_ptr(),
3214 data.len(),
3215 out,
3216 )
3217 })
3218 }
3219
3220 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
3222 self.emit(|b, out| unsafe {
3223 ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out)
3224 })
3225 }
3226
3227 pub fn add_citation(&mut self, label: &str) -> Result<NodeId, Error> {
3232 self.emit(|b, out| unsafe {
3233 ffi::twig_builder_add_citation(b, label.as_ptr(), label.len(), out)
3234 })
3235 }
3236
3237 pub fn add_substitution(&mut self, label: &str) -> Result<NodeId, Error> {
3241 self.emit(|b, out| unsafe {
3242 ffi::twig_builder_add_substitution(b, label.as_ptr(), label.len(), out)
3243 })
3244 }
3245
3246 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
3248 self.emit(|b, out| unsafe {
3249 ffi::twig_builder_add_reference(
3250 b,
3251 label.as_ptr(),
3252 label.len(),
3253 destination.as_ptr(),
3254 destination.len(),
3255 out,
3256 )
3257 })
3258 }
3259
3260 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
3262 self.emit(|b, out| unsafe {
3263 ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out)
3264 })
3265 }
3266
3267 pub fn add_ordered_list(
3269 &mut self,
3270 numbering: OrderedNumbering,
3271 delim: OrderedDelim,
3272 tight: bool,
3273 start: Option<u32>,
3274 ) -> Result<NodeId, Error> {
3275 let (start_val, has_start) = match start {
3276 Some(s) => (s, 1),
3277 None => (0, 0),
3278 };
3279 self.emit(|b, out| unsafe {
3280 ffi::twig_builder_add_ordered_list(
3281 b,
3282 numbering.to_c(),
3283 delim.to_c(),
3284 tight as c_int,
3285 start_val,
3286 has_start,
3287 out,
3288 )
3289 })
3290 }
3291
3292 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
3294 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
3295 }
3296
3297 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
3299 self.emit(|b, out| unsafe {
3300 ffi::twig_builder_add_task_list_item(b, checked as c_int, out)
3301 })
3302 }
3303
3304 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
3306 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
3307 }
3308
3309 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
3311 self.emit(|b, out| unsafe {
3312 ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out)
3313 })
3314 }
3315
3316 pub fn add_cell_spanning(
3321 &mut self,
3322 head: bool,
3323 alignment: Alignment,
3324 colspan: u32,
3325 rowspan: u32,
3326 ) -> Result<NodeId, Error> {
3327 self.emit(|b, out| unsafe {
3328 ffi::twig_builder_add_cell_spanning(
3329 b,
3330 head as c_int,
3331 alignment.to_c(),
3332 colspan,
3333 rowspan,
3334 out,
3335 )
3336 })
3337 }
3338
3339 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
3342 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
3343 let status = unsafe {
3344 ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len())
3345 };
3346 Error::from_status(status)
3347 }
3348
3349 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
3353 let kvs: Vec<ffi::TwigKeyVal> = attrs
3354 .iter()
3355 .map(|(k, v)| ffi::TwigKeyVal {
3356 key: k.as_ptr(),
3357 key_len: k.len(),
3358 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
3359 value_len: v.map_or(0, |s| s.len()),
3360 })
3361 .collect();
3362 let status = unsafe {
3363 ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len())
3364 };
3365 Error::from_status(status)
3366 }
3367
3368 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3371 let raw = self.raw.as_ptr();
3372 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
3373 }
3374
3375 pub fn serialize_to(&mut self, root: NodeId, target: Target) -> Result<Vec<u8>, Error> {
3382 let raw = self.raw.as_ptr();
3383 let ffi_target: ffi::TwigFormat = target.into();
3384 collect_bytes(|ptr, len| unsafe {
3385 ffi::twig_builder_serialize(raw, root.0, ffi_target as i32, ptr, len)
3386 })
3387 }
3388
3389 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
3394 self.serialize_to(root, format.into())
3395 }
3396
3397 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3399 let raw = self.raw.as_ptr();
3400 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
3401 }
3402
3403 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
3406 let raw = self.raw.as_ptr();
3407 collect_matches(|ptr, len| unsafe {
3408 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
3409 })
3410 }
3411
3412 fn emit(
3415 &mut self,
3416 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
3417 ) -> Result<NodeId, Error> {
3418 let mut id: u32 = 0;
3419 let status = call(self.raw.as_ptr(), &mut id);
3420 Error::from_status(status)?;
3421 Ok(NodeId(id))
3422 }
3423}
3424
3425impl Drop for Builder {
3426 fn drop(&mut self) {
3427 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
3428 }
3429}
3430
3431#[cfg(test)]
3432mod tests {
3433 use super::*;
3434
3435 #[test]
3436 fn abi_version_matches() {
3437 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
3441 }
3442
3443 #[test]
3444 fn parses_and_renders_markdown_html() {
3445 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3446 let html = doc.render_html().expect("render html");
3447 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
3448 }
3449
3450 #[test]
3451 fn parses_html_input() {
3452 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
3453 let html = doc.render_html().expect("render html");
3454 assert!(String::from_utf8_lossy(&html).contains("hi"));
3455 }
3456
3457 #[test]
3458 fn parses_renders_and_writes_asciidoc() {
3459 let mut doc = Document::parse_str("= Title\n\nsome *bold* text\n", Format::Asciidoc)
3460 .expect("parse asciidoc");
3461 let html = String::from_utf8_lossy(&doc.render_html().expect("render html")).into_owned();
3462 assert!(html.contains("<h1>Title</h1>"), "got {html:?}");
3463 assert!(html.contains("<strong>bold</strong>"), "got {html:?}");
3464
3465 let back = doc.serialize_to(Target::Asciidoc).expect("serialize asciidoc");
3467 assert_eq!(String::from_utf8_lossy(&back), "= Title\n\nsome *bold* text\n");
3468 let mut md = Document::parse_str("# Title\n\nsome **bold** text\n", Format::Markdown)
3469 .expect("parse markdown");
3470 let converted = md.serialize_to(Target::Asciidoc).expect("convert to asciidoc");
3471 assert_eq!(String::from_utf8_lossy(&converted), "= Title\n\nsome *bold* text\n");
3472 assert_eq!(Target::from(Format::Asciidoc), Target::Asciidoc);
3473 assert_eq!(Target::Asciidoc.as_format(), Some(Format::Asciidoc));
3474 }
3475
3476 #[test]
3477 fn serialize_round_trips_and_cross_converts() {
3478 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3479
3480 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
3481 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
3482
3483 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
3485 }
3486
3487 #[test]
3488 fn serialize_markdown_to_djot() {
3489 let mut doc =
3490 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3491 let djot = doc.serialize(Format::Djot).expect("serialize djot");
3492 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3493 }
3494
3495 #[test]
3496 fn serialize_to_takes_the_output_axis() {
3497 let mut doc =
3498 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3499
3500 let djot = doc.serialize_to(Target::Djot).expect("serialize djot");
3501 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3502
3503 assert_eq!(doc.serialize_to(Target::Xml), Err(Error::UnsupportedFormat));
3506 }
3507
3508 #[test]
3509 fn serialize_and_serialize_to_agree() {
3510 let mut a = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3513 let mut b = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3514 for format in [Format::Markdown, Format::Djot, Format::Html] {
3515 assert_eq!(a.serialize(format), b.serialize_to(Target::from(format)));
3516 }
3517 }
3518
3519 #[test]
3520 fn every_format_is_a_target_that_names_it_back() {
3521 for format in [Format::Djot, Format::Markdown, Format::Xml, Format::Html] {
3524 assert_eq!(Target::from(format).as_format(), Some(format));
3525 }
3526 }
3527
3528 #[test]
3529 fn ast_json_dumps_the_tree() {
3530 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
3531 let json = doc.ast_json().expect("ast json");
3532 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
3533 }
3534
3535 #[test]
3536 fn query_finds_nodes_by_selector() {
3537 let source = "# One\n\n## Two\n";
3538 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3539 let matches = doc.query("heading").expect("query");
3540
3541 assert_eq!(matches.len(), 2);
3542 for m in &matches {
3543 assert_eq!(m.kind, Kind::Heading);
3544 assert!(m.span.start < m.span.end);
3545 }
3546 }
3547
3548 #[test]
3549 fn query_recovers_code_spans() {
3550 let source = "prose `code` more prose\n";
3551 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3552 let matches = doc.query("verbatim").expect("query");
3553
3554 assert_eq!(matches.len(), 1);
3555 assert_eq!(&source[matches[0].span.clone()], "`code`");
3556 }
3557
3558 #[test]
3559 fn document_span_accessors_read_by_node_id() {
3560 let source = "# hi\n\ntext\n";
3561 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3562 let heading = doc.query("heading").expect("query").pop().expect("heading");
3563
3564 assert_eq!(
3565 doc.span(NodeId(heading.node_id)).expect("span"),
3566 heading.span
3567 );
3568 assert_eq!(
3569 doc.content_span(NodeId(heading.node_id))
3570 .expect("content span"),
3571 heading.content_span
3572 );
3573 assert_eq!(doc.span(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3574 }
3575
3576 #[test]
3577 fn document_walks_its_tree_without_an_editor() {
3578 let source = "# hi\n\ntext\n";
3579 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3580
3581 let nodes = doc.nodes().expect("nodes");
3582 assert!(nodes.len() >= 3);
3583 for (i, n) in nodes.iter().enumerate() {
3584 assert_eq!(n.id, NodeId(i as u32));
3585 }
3586
3587 let kids = doc.children(None).expect("children");
3588 assert_eq!(kids.len(), 2);
3589 assert_eq!(kids[0].kind, Kind::Heading);
3590
3591 let sub = doc.subtree(NodeId(kids[0].node_id)).expect("subtree");
3592 assert_eq!(sub[0].id, NodeId(0));
3593 assert_eq!(sub[0].parent, None);
3594 assert_eq!(sub[0].span, kids[0].span);
3595
3596 let hit = doc.node_at(2).expect("node_at").expect("a node at 2");
3597 let chain = doc.ancestors_at(2).expect("ancestors");
3598 assert_eq!(chain.last().expect("deepest").node_id, hit.node_id);
3599 assert_eq!(chain[0].kind, Kind::Doc);
3600
3601 assert_eq!(doc.subtree(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3602 }
3603
3604 #[test]
3605 fn editor_document_view_reads_the_live_tree() {
3606 let mut ed = Editor::new_str("# one\n\ntwo\n", Format::Markdown).expect("editor");
3607
3608 {
3609 let mut view = ed.document().expect("view");
3610 let kids = view.children(None).expect("children");
3611 assert_eq!(kids.len(), 2);
3612 assert_eq!(kids[0].kind, Kind::Heading);
3613 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..5);
3614 assert_eq!(view.render_html(), Err(Error::UnsupportedFormat));
3616 assert_eq!(
3617 view.serialize(Format::Markdown),
3618 Err(Error::UnsupportedFormat)
3619 );
3620 }
3621
3622 ed.replace("0", "# one and a half").expect("replace");
3623 let mut view = ed.document().expect("view");
3624 let kids = view.children(None).expect("children");
3625 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..16);
3626 }
3627
3628 #[test]
3629 fn query_rejects_a_malformed_selector() {
3630 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
3631 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
3632 }
3633
3634 #[test]
3635 fn editor_edits_by_index_path() {
3636 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
3637 ed.replace_content("0.0", "bye").expect("replace_content");
3638 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
3639 }
3640
3641 #[test]
3642 fn flat_nodes_expose_element_name_and_attrs() {
3643 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
3647 let mut ed = Editor::new_ext(
3648 src.as_bytes(),
3649 Format::Markdown,
3650 MarkdownExtensions {
3651 html_elements: true,
3652 ..Default::default()
3653 },
3654 )
3655 .expect("editor");
3656 let nodes = ed.nodes().expect("nodes");
3657
3658 let source = nodes
3659 .iter()
3660 .find(|n| n.name.as_deref() == Some("source"))
3661 .expect("a <source> element node");
3662 assert_eq!(
3663 source.attrs,
3664 vec![
3665 (
3666 "media".to_string(),
3667 Some("(prefers-color-scheme: dark)".to_string())
3668 ),
3669 ("srcset".to_string(), Some("d.svg".to_string())),
3670 ]
3671 );
3672
3673 let img = nodes
3676 .iter()
3677 .find(|n| n.kind == Kind::Image)
3678 .expect("an image node");
3679 assert!(img.name.is_none());
3680 assert_eq!(img.destination.as_deref(), Some("l.svg"));
3681
3682 let picture_kids_str = nodes.iter().find(|n| n.kind == Kind::Str);
3684 if let Some(s) = picture_kids_str {
3685 assert!(s.name.is_none() && s.attrs.is_empty());
3686 }
3687 }
3688
3689 #[test]
3690 fn definitions_finds_what_a_walk_from_the_root_cannot() {
3691 let mut doc = Document::parse_str(
3695 "text[^1] [x][a]\n\n[^1]: note\n\n[a]: /u\n",
3696 Format::Markdown,
3697 )
3698 .expect("parse markdown");
3699
3700 let defs = doc.definitions().expect("definitions");
3701 let mut kinds: Vec<Kind> = defs.iter().map(|m| m.kind.clone()).collect();
3702 kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3703 assert_eq!(kinds, vec![Kind::Footnote, Kind::Reference]);
3704
3705 let all = doc.nodes().expect("nodes");
3708 let root = all
3709 .iter()
3710 .find(|n| n.kind == Kind::Doc)
3711 .expect("a doc root");
3712 let mut reachable = vec![root.id];
3713 let mut i = 0;
3714 while i < reachable.len() {
3715 let n = &all[reachable[i].0 as usize];
3716 let mut c = n.first_child;
3717 while let Some(cid) = c {
3718 reachable.push(cid);
3719 c = all[cid.0 as usize].next_sibling;
3720 }
3721 i += 1;
3722 }
3723 for d in &defs {
3724 assert!(
3725 !reachable.contains(&NodeId(d.node_id)),
3726 "{} should be unreachable from the root",
3727 d.kind
3728 );
3729 }
3730
3731 let mut plain = Document::parse_str("just text\n", Format::Markdown).expect("parse");
3733 assert_eq!(plain.definitions().expect("definitions"), Vec::new());
3734 }
3735
3736 #[test]
3737 fn kind_round_trips_through_its_published_name() {
3738 for k in [
3742 Kind::Doc,
3743 Kind::Para,
3744 Kind::Heading,
3745 Kind::Container,
3746 Kind::TaskListItem,
3747 Kind::Superscript,
3748 Kind::FootnoteReference,
3749 Kind::ProcessingInstruction,
3750 Kind::Cdata,
3751 ] {
3752 assert_eq!(Kind::from(k.as_str()), k, "{k} did not round-trip");
3753 assert!(!k.is_unknown());
3754 }
3755 }
3756
3757 #[test]
3758 fn an_unknown_kind_name_is_carried_rather_than_lost() {
3759 let k = Kind::from("some_future_kind");
3762 assert!(k.is_unknown());
3763 assert_eq!(k.as_str(), "some_future_kind");
3764 assert_eq!(k, Kind::Other("some_future_kind".to_string()));
3765 }
3766
3767 #[test]
3768 fn every_kind_the_library_publishes_has_a_variant() {
3769 let cases: &[(&str, Format, MarkdownExtensions)] = &[
3774 (
3775 "# 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",
3776 Format::Markdown,
3777 MarkdownExtensions {
3778 directives: false,
3779 math: false,
3780 html_elements: false,
3781 highlight: false,
3782 highlight_colors: false,
3783 },
3784 ),
3785 (
3786 "| 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",
3787 Format::Markdown,
3788 MarkdownExtensions::default(),
3789 ),
3790 (
3791 ":::note\nbody\n:::\n\n:role[x]\n\n$a+b$ ==h== ==🔴 r==\n",
3792 Format::Markdown,
3793 MarkdownExtensions {
3794 directives: true,
3795 math: true,
3796 html_elements: false,
3797 highlight: true,
3798 highlight_colors: true,
3799 },
3800 ),
3801 (
3802 "a^b^ c~d~ {=e=} {+f+} {-g-} 'q' \"dq\"\n\n\n\n<https://e.com>\n",
3803 Format::Djot,
3804 MarkdownExtensions::default(),
3805 ),
3806 (
3807 "<!-- c --><!DOCTYPE html><video controls><p>x</p></video>",
3808 Format::Html,
3809 MarkdownExtensions::default(),
3810 ),
3811 ];
3812
3813 let mut unknown: Vec<String> = Vec::new();
3814 let mut seen: Vec<String> = Vec::new();
3815 for (src, format, ext) in cases {
3816 let mut ed = Editor::new_ext(src.as_bytes(), *format, *ext).expect("editor");
3817 for n in ed.nodes().expect("nodes") {
3818 if n.kind.is_unknown() {
3819 unknown.push(n.kind.as_str().to_string());
3820 }
3821 seen.push(n.kind.as_str().to_string());
3822 }
3823 }
3824 unknown.sort();
3825 unknown.dedup();
3826 assert!(unknown.is_empty(), "kinds with no variant: {unknown:?}");
3827
3828 seen.sort();
3831 seen.dedup();
3832 assert!(
3833 seen.len() >= 30,
3834 "only {} distinct kinds reached: {seen:?}",
3835 seen.len()
3836 );
3837 }
3838
3839 #[test]
3840 fn diagnostics_report_what_a_conversion_would_lose() {
3841 let mut doc = Document::parse_str("a^b^ c\n", Format::Djot).expect("parse djot");
3845
3846 let to_md = doc
3847 .diagnostics(Target::Markdown)
3848 .expect("markdown diagnostics");
3849 assert_eq!(
3850 to_md,
3851 vec![Warning {
3852 fidelity: Fidelity::Degraded,
3853 path: "0/1".to_string(),
3854 kind: Kind::Superscript,
3855 }]
3856 );
3857
3858 assert_eq!(
3860 doc.diagnostics(Target::Djot).expect("djot diagnostics"),
3861 Vec::new()
3862 );
3863 }
3864
3865 #[test]
3866 fn diagnostics_separate_a_droppable_node_from_a_degradable_one() {
3867 let mut doc =
3871 Document::parse_str("<p>hi</p><!-- secret -->", Format::Html).expect("parse html");
3872 let warnings = doc.diagnostics(Target::Djot).expect("djot diagnostics");
3873 let comment = warnings
3874 .iter()
3875 .find(|w| w.kind == Kind::Comment)
3876 .expect("a warning about the comment");
3877 assert_eq!(comment.fidelity, Fidelity::Dropped);
3878 }
3879
3880 #[test]
3881 fn diagnostics_refuse_a_target_with_no_serializer() {
3882 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3885 assert_eq!(doc.diagnostics(Target::Xml), Err(Error::UnsupportedFormat));
3886 assert!(doc.diagnostics(Target::Asciidoc).is_ok());
3888 }
3889
3890 #[test]
3891 fn diagnostics_flag_a_header_less_table_and_leave_a_headed_one_alone() {
3892 let mut headed = Document::parse_str(
3897 "<table><tr><th>H</th></tr><tr><td>a</td></tr></table>",
3898 Format::Html,
3899 )
3900 .expect("parse headed table");
3901 assert!(
3902 headed
3903 .diagnostics(Target::Markdown)
3904 .expect("diagnostics")
3905 .iter()
3906 .all(|w| w.kind != Kind::Table)
3907 );
3908
3909 let mut headless = Document::parse_str("<table><tr><td>a</td></tr></table>", Format::Html)
3910 .expect("parse header-less table");
3911 let table_warning = headless
3912 .diagnostics(Target::Markdown)
3913 .expect("diagnostics")
3914 .into_iter()
3915 .find(|w| w.kind == Kind::Table)
3916 .expect("a warning about the table");
3917 assert_eq!(table_warning.fidelity, Fidelity::Degraded);
3918 }
3919
3920 #[test]
3921 fn container_origin_separates_a_div_from_a_div() {
3922 let mut html =
3927 Editor::new("<div>hi</div>\n".as_bytes(), Format::Html).expect("html editor");
3928 let mut md = Editor::new_ext(
3929 ":::div\nhi\n:::\n".as_bytes(),
3930 Format::Markdown,
3931 MarkdownExtensions {
3932 directives: true,
3933 ..Default::default()
3934 },
3935 )
3936 .expect("markdown editor");
3937
3938 let html_nodes = html.nodes().expect("html nodes");
3939 let md_nodes = md.nodes().expect("markdown nodes");
3940 let tag = html_nodes
3941 .iter()
3942 .find(|n| n.name.as_deref() == Some("div"))
3943 .expect("a <div> container");
3944 let directive = md_nodes
3945 .iter()
3946 .find(|n| n.name.as_deref() == Some("div"))
3947 .expect("a :::div container");
3948
3949 assert_eq!(tag.kind, directive.kind);
3951 assert_eq!(tag.name, directive.name);
3952 assert_eq!(tag.directive_form, directive.directive_form);
3953 assert_eq!(tag.directive_form, Some(DirectiveForm::Container));
3954
3955 assert_eq!(tag.origin, Some(ContainerOrigin::Element));
3957 assert_eq!(directive.origin, Some(ContainerOrigin::Directive));
3958 }
3959
3960 fn for_both_formats(src: &str, check: impl Fn(&mut Document, Format)) {
3964 for format in [Format::Markdown, Format::Djot] {
3965 let mut doc = Document::parse(src.as_bytes(), format).expect("parse");
3966 check(&mut doc, format);
3967 }
3968 }
3969
3970 #[test]
3971 fn marker_span_is_what_a_rich_view_hides() {
3972 for_both_formats("> - [x] done\n", |doc, format| {
3973 let nodes = doc.nodes().expect("nodes");
3974 let quote = nodes
3975 .iter()
3976 .find(|n| n.kind == Kind::BlockQuote)
3977 .expect("a block quote");
3978 let item = nodes
3979 .iter()
3980 .find(|n| n.kind == Kind::TaskListItem)
3981 .expect("a task item");
3982
3983 assert_eq!(quote.marker_span, Some(0..2), "{format:?}");
3987 assert_eq!(item.marker_span, Some(2..8), "{format:?}");
3988
3989 assert_eq!(quote.content_span, Some(quote.span.clone()), "{format:?}");
3993
3994 let para = nodes
3996 .iter()
3997 .find(|n| n.kind == Kind::Para)
3998 .expect("a paragraph");
3999 assert_eq!(para.marker_span, None, "{format:?}");
4000 });
4001 }
4002
4003 #[test]
4004 fn attrs_span_locates_the_attribute_block_a_heuristic_had_to_guess_at() {
4005 let src = "{.vis .family}\nheld back\n\nplain\n";
4011 let mut doc = Document::parse(src.as_bytes(), Format::Djot).expect("parse");
4012 let nodes = doc.nodes().expect("nodes");
4013 let paras: Vec<&FlatNode> = nodes.iter().filter(|n| n.kind == Kind::Para).collect();
4014 assert_eq!(paras.len(), 2);
4015
4016 let span = doc
4017 .attrs_span(paras[0].id)
4018 .expect("attrs span")
4019 .expect("the attributed paragraph has one");
4020 assert_eq!(&src[span.clone()], "{.vis .family}");
4021 assert!(span.end <= paras[0].span.start);
4024
4025 assert_eq!(doc.attrs_span(paras[1].id).expect("attrs span"), None);
4028 }
4029
4030 #[test]
4031 fn line_prefix_assembles_every_marker_on_the_line() {
4032 for_both_formats("> - [x] done\n", |doc, format| {
4033 assert_eq!(doc.line_prefix(9).expect("prefix"), Some(0..8), "{format:?}");
4036 });
4037 }
4038
4039 #[test]
4040 fn line_prefix_is_none_on_a_continuation_line() {
4041 for_both_formats("> c\n> d\n", |doc, format| {
4047 assert_eq!(doc.line_prefix(2).expect("prefix"), Some(0..2), "{format:?}");
4048 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
4049 });
4050 }
4051
4052 #[test]
4053 fn a_caret_at_a_blocks_end_is_in_that_block_in_both_formats() {
4054 for_both_formats("a\n\nb\n", |doc, format| {
4060 for offset in [0usize, 1, 3, 4] {
4061 let hit = doc
4062 .node_at_caret(offset)
4063 .expect("caret hit")
4064 .expect("some node");
4065 assert_eq!(hit.kind, Kind::Str, "{format:?} at {offset}");
4066 }
4067 for offset in [2usize, 5] {
4070 let hit = doc
4071 .node_at_caret(offset)
4072 .expect("caret hit")
4073 .expect("some node");
4074 assert_eq!(hit.kind, Kind::Doc, "{format:?} at {offset}");
4075 }
4076 });
4077 }
4078
4079 #[test]
4080 fn the_caret_chain_ends_at_the_node_the_scalar_call_returns() {
4081 for_both_formats("- a\n", |doc, format| {
4082 let hit = doc.node_at_caret(3).expect("hit").expect("some node");
4083 let chain = doc.ancestors_at_caret(3).expect("chain");
4084 assert_eq!(chain.last().map(|m| m.node_id), Some(hit.node_id), "{format:?}");
4085 assert!(
4088 chain.iter().any(|m| m.kind == Kind::ListItem),
4089 "{format:?}: chain should reach the list item"
4090 );
4091 });
4092 }
4093
4094 #[test]
4095 fn continuation_prefix_repeats_a_quote_and_indents_past_an_item() {
4096 for_both_formats("> - a\n", |doc, format| {
4097 assert_eq!(doc.line_prefix(4).expect("prefix"), Some(0..4), "{format:?}");
4102 let cont = doc.continuation_prefix(4).expect("continuation");
4103 assert_eq!(cont.text, "> ", "{format:?}");
4104 assert_eq!(cont.columns, 4, "{format:?}");
4105 });
4106 }
4107
4108 #[test]
4109 fn continuation_prefix_answers_on_a_line_that_opens_nothing() {
4110 for_both_formats("> c\n> d\n", |doc, format| {
4113 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
4114 assert_eq!(
4115 doc.continuation_prefix(6).expect("continuation").text,
4116 "> ",
4117 "{format:?}"
4118 );
4119 });
4120 }
4121
4122 #[test]
4123 fn continuation_prefix_takes_an_ordered_markers_own_width() {
4124 for_both_formats("10. x\n", |doc, format| {
4127 assert_eq!(
4128 doc.continuation_prefix(4).expect("continuation").columns,
4129 4,
4130 "{format:?}"
4131 );
4132 });
4133 for_both_formats("1. x\n", |doc, format| {
4134 assert_eq!(
4135 doc.continuation_prefix(3).expect("continuation").columns,
4136 3,
4137 "{format:?}"
4138 );
4139 });
4140 }
4141
4142 #[test]
4143 fn a_blank_line_keeps_a_quote_alive_and_drops_an_items_indent() {
4144 for_both_formats("> - a\n", |doc, format| {
4145 let blank = doc.blank_line_prefix(4).expect("blank");
4146 assert_eq!(blank.text, ">", "{format:?}");
4149 assert_eq!(blank.columns, 1, "{format:?}");
4150 });
4151 for_both_formats("- a\n", |doc, format| {
4154 assert_eq!(doc.blank_line_prefix(3).expect("blank").text, "", "{format:?}");
4155 });
4156 }
4157
4158 #[test]
4159 fn a_prefix_column_count_is_not_its_byte_length() {
4160 let mut doc = Document::parse("- x
4163".as_bytes(), Format::Markdown).expect("parse");
4164 let cont = doc.continuation_prefix(2).expect("continuation");
4165 assert_eq!(cont.columns, 4);
4166 }
4167
4168 #[test]
4169 fn set_block_opens_a_heading_on_a_blank_line() {
4170 for format in [Format::Markdown, Format::Djot] {
4171 let mut ed = Editor::new("a\n\n".as_bytes(), format).expect("editor");
4172 ed.set_block(3, BlockKind::Heading(2)).expect("set_block");
4173 assert_eq!(ed.source().expect("source"), b"a\n\n## ", "{format:?}");
4174 let nodes = ed.nodes().expect("nodes");
4178 assert!(
4179 nodes.iter().any(|n| n.kind == Kind::Heading),
4180 "{format:?}: should have parsed a heading"
4181 );
4182 }
4183 }
4184
4185 #[test]
4186 fn set_block_refuses_a_blank_line_inside_a_code_block() {
4187 for format in [Format::Markdown, Format::Djot] {
4191 let src = "```\nx\n\ny\n```\n";
4192 let mut ed = Editor::new(src.as_bytes(), format).expect("editor");
4193 let blank = src.find("\n\n").expect("a blank line") + 1;
4194 assert!(
4195 matches!(
4196 ed.set_block(blank, BlockKind::Heading(1)),
4197 Err(Error::NotEditable)
4198 ),
4199 "{format:?}"
4200 );
4201 assert_eq!(ed.source().expect("source"), src.as_bytes(), "{format:?}");
4202 }
4203 }
4204
4205 #[test]
4206 fn task_items_report_their_checkbox_state() {
4207 for_both_formats("- [ ] a\n- [x] b\n- [X] c\n- d\n", |doc, format| {
4211 let nodes = doc.nodes().expect("nodes");
4212 let states: Vec<Option<bool>> = nodes
4213 .iter()
4214 .filter(|n| matches!(n.kind, Kind::TaskListItem | Kind::ListItem))
4215 .map(|n| n.checked)
4216 .collect();
4217 assert_eq!(
4218 states,
4219 vec![Some(false), Some(true), Some(true), None],
4220 "{format:?}"
4221 );
4222
4223 for n in nodes.iter().filter(|n| n.kind == Kind::Para) {
4226 assert_eq!(n.checked, None, "{format:?}");
4227 }
4228 });
4229 }
4230
4231 #[test]
4232 fn an_editor_reaches_the_caret_reads_through_its_document_view() {
4233 let mut ed = Editor::new("- a\n".as_bytes(), Format::Markdown).expect("editor");
4238 let mut view = ed.document().expect("document view");
4239
4240 assert_eq!(view.line_prefix(3).expect("prefix"), Some(0..2));
4241 let hit = view.node_at_caret(3).expect("hit").expect("some node");
4242 assert_eq!(hit.kind, Kind::Str);
4243 }
4244
4245 #[test]
4246 fn container_origin_is_none_for_non_containers() {
4247 let mut ed = Editor::new("# hi\n\npara\n".as_bytes(), Format::Markdown).expect("editor");
4250 for n in ed.nodes().expect("nodes") {
4251 assert_eq!(n.origin, None, "{} should carry no origin", n.kind);
4252 }
4253 }
4254
4255 #[test]
4256 fn flat_nodes_expose_directive_name_and_form() {
4257 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
4263 let mut ed = Editor::new_ext(
4264 src.as_bytes(),
4265 Format::Markdown,
4266 MarkdownExtensions {
4267 directives: true,
4268 ..Default::default()
4269 },
4270 )
4271 .expect("editor");
4272 let nodes = ed.nodes().expect("nodes");
4273
4274 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
4275 .iter()
4276 .filter(|n| n.kind == Kind::Container)
4277 .map(|n| (n.name.as_deref(), n.directive_form))
4278 .collect();
4279 assert_eq!(
4280 forms,
4281 vec![
4282 (Some("note"), Some(DirectiveForm::Container)),
4283 (Some("embed"), Some(DirectiveForm::Leaf)),
4284 (Some("abbr"), Some(DirectiveForm::Text)),
4285 ]
4286 );
4287
4288 let embed = nodes
4291 .iter()
4292 .find(|n| n.name.as_deref() == Some("embed"))
4293 .expect("embed");
4294 assert_eq!(
4295 embed.attrs,
4296 vec![("src".to_string(), Some("demo.html".to_string()))]
4297 );
4298 let para = nodes.iter().find(|n| n.kind == Kind::Para).expect("a para");
4299 assert!(para.directive_form.is_none() && para.name.is_none());
4300 }
4301
4302 #[test]
4303 fn editor_insert_child_and_delete() {
4304 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
4305 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4306 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
4307 ed.delete("0.1").expect("delete");
4308 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
4309 }
4310
4311 #[test]
4312 fn editor_edits_by_selector() {
4313 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4314 ed.replace("heading(\"Two\")", "## Renamed")
4315 .expect("replace");
4316 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
4317 }
4318
4319 #[test]
4320 fn editor_locator_errors_are_distinct() {
4321 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
4322 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
4323 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
4324 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
4325 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
4327 }
4328
4329 #[test]
4330 fn editor_reparse_break_rolls_back() {
4331 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
4332 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
4333 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
4334 }
4335
4336 #[test]
4337 fn editor_leaf_content_is_not_editable() {
4338 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4339 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
4340 }
4341
4342 #[test]
4343 fn editor_query_reflects_current_tree() {
4344 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
4345 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4346 assert_eq!(ed.query("element").expect("query").len(), 3);
4348 let json = ed.ast_json().expect("ast_json");
4349 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
4350 }
4351
4352 #[test]
4355 fn editor_edit_range_types_backspaces_and_reports_change() {
4356 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4357
4358 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
4360 assert_eq!(ed.source_str().unwrap(), "aXb\n");
4361 assert_eq!(c.old, 1..1);
4362 assert_eq!(c.new, 1..2);
4363 assert_eq!(c.delta(), 1);
4364
4365 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
4367 assert_eq!(ed.source_str().unwrap(), "ab\n");
4368 assert_eq!(c2.old, 1..2);
4369 assert_eq!(c2.new, 1..1);
4370 assert_eq!(c2.delta(), -1);
4371 }
4372
4373 #[test]
4374 fn editor_edit_range_rejects_bad_ranges() {
4375 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4376 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"); }
4380
4381 #[test]
4382 fn editor_last_change_reports_locator_ops_too() {
4383 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4384 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed")
4387 .expect("replace");
4388 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
4389 let c = ed.last_change().expect("a change was recorded");
4390 assert_eq!(c.old, 7..13);
4392 assert_eq!(c.new, 7..17);
4393 }
4394
4395 #[test]
4396 fn editor_nodes_is_a_walkable_flat_tree() {
4397 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4398 let nodes = ed.nodes().expect("nodes");
4399 assert!(!nodes.is_empty());
4400
4401 for (i, n) in nodes.iter().enumerate() {
4403 assert_eq!(n.id, NodeId(i as u32));
4404 }
4405 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
4407 assert_eq!(roots.len(), 1);
4408 assert_eq!(roots[0].kind, Kind::Doc);
4409
4410 let heading = nodes
4412 .iter()
4413 .find(|n| n.kind == Kind::Heading)
4414 .expect("a heading");
4415 assert_eq!(heading.level, Some(1));
4416 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
4417
4418 assert_eq!(heading.head, None);
4420 assert_eq!(heading.alignment, None);
4421
4422 for n in nodes.iter().filter(|n| n.parent.is_some()) {
4425 let p = &nodes[n.parent.unwrap().0 as usize];
4426 let mut kid = p.first_child;
4427 let mut seen = false;
4428 while let Some(NodeId(k)) = kid {
4429 if k == n.id.0 {
4430 seen = true;
4431 break;
4432 }
4433 kid = nodes[k as usize].next_sibling;
4434 }
4435 assert!(
4436 seen,
4437 "node {:?} not found among its parent's children",
4438 n.id
4439 );
4440 }
4441 }
4442
4443 #[test]
4444 fn editor_child_spans_and_subtree_agree_with_nodes() {
4445 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
4446 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4447 let all = ed.nodes().expect("nodes");
4448 let doc = all.iter().find(|n| n.kind == Kind::Doc).expect("doc");
4449
4450 let top = ed.child_spans(None).expect("child_spans");
4453 let mut want = Vec::new();
4454 let mut c = doc.first_child;
4455 while let Some(id) = c {
4456 want.push(id);
4457 c = all[id.0 as usize].next_sibling;
4458 }
4459 assert_eq!(top.len(), want.len(), "top-level count");
4460 for (m, id) in top.iter().zip(&want) {
4461 assert_eq!(m.node_id, id.0, "child id");
4462 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
4463 assert_eq!(m.span, all[id.0 as usize].span, "child span");
4464 }
4465 assert!(
4467 src[top[0].span.clone()].starts_with('#'),
4468 "first block is the heading"
4469 );
4470
4471 let list = top
4473 .iter()
4474 .find(|m| {
4475 matches!(
4476 m.kind,
4477 Kind::BulletList | Kind::OrderedList | Kind::TaskList
4478 )
4479 })
4480 .expect("a list");
4481 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
4482 assert_eq!(items.len(), 2);
4483 assert!(
4484 items.iter().all(|m| m.kind == Kind::ListItem),
4485 "items: {items:?}"
4486 );
4487
4488 let para = top
4490 .iter()
4491 .find(|m| m.kind == Kind::Para)
4492 .expect("a para")
4493 .node_id;
4494 let sub = ed.subtree(NodeId(para)).expect("subtree");
4495 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
4496 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
4497 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
4498 assert_eq!(sub[0].kind, Kind::Para);
4499 for (i, n) in sub.iter().enumerate() {
4500 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
4501 for link in [n.parent, n.first_child, n.next_sibling]
4502 .into_iter()
4503 .flatten()
4504 {
4505 assert!(
4506 (link.0 as usize) < sub.len(),
4507 "link {link:?} escapes the subtree"
4508 );
4509 }
4510 }
4511 assert!(
4512 src[sub[0].span.clone()].starts_with("Hello"),
4513 "absolute span: {:?}",
4514 &src[sub[0].span.clone()]
4515 );
4516
4517 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<Kind> {
4519 let mut out = Vec::new();
4520 let mut stack = vec![root];
4521 while let Some(id) = stack.pop() {
4522 let n = &all[id.0 as usize];
4523 out.push(n.kind.clone());
4524 let mut c = n.first_child;
4525 while let Some(cid) = c {
4526 stack.push(cid);
4527 c = all[cid.0 as usize].next_sibling;
4528 }
4529 }
4530 out
4531 }
4532 let mut want_kinds = arena_kinds(&all, NodeId(para));
4533 let mut got_kinds: Vec<Kind> = sub.iter().map(|n| n.kind.clone()).collect();
4534 want_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4538 got_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4539 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
4540
4541 assert!(matches!(
4543 ed.subtree(NodeId(9999)),
4544 Err(Error::InvalidArgument)
4545 ));
4546 }
4547
4548 #[test]
4549 fn flat_nodes_carry_table_head_and_alignment() {
4550 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
4554 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4555 let nodes = ed.nodes().expect("nodes");
4556
4557 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Row).collect();
4558 assert_eq!(rows.len(), 2, "a header row and one body row");
4559 assert_eq!(rows[0].head, Some(true), "first row is the header");
4560 assert_eq!(rows[1].head, Some(false), "second row is a body row");
4561
4562 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Cell).collect();
4563 assert_eq!(cells.len(), 4);
4564 assert_eq!(cells[0].alignment, Some(Alignment::Left));
4566 assert_eq!(cells[1].alignment, Some(Alignment::Right));
4567 assert_eq!(cells[2].alignment, Some(Alignment::Left));
4568 assert_eq!(cells[3].alignment, Some(Alignment::Right));
4569 assert_eq!(cells[0].head, Some(true));
4571 assert_eq!(cells[2].head, Some(false));
4572
4573 let mut plain =
4576 Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
4577 let pnodes = plain.nodes().expect("nodes");
4578 let pcell = pnodes
4579 .iter()
4580 .find(|n| n.kind == Kind::Cell)
4581 .expect("a cell");
4582 assert_eq!(pcell.alignment, Some(Alignment::Default));
4583 }
4584
4585 #[test]
4586 fn cell_extent_reports_merged_cells_and_nothing_else() {
4587 let src = "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td><td>b</td></tr></table>";
4588 let mut doc = Document::parse_str(src, Format::Html).expect("parse");
4589 let cells: Vec<NodeId> = doc
4590 .nodes()
4591 .expect("nodes")
4592 .iter()
4593 .filter(|n| n.kind == Kind::Cell)
4594 .map(|n| n.id)
4595 .collect();
4596 assert_eq!(cells.len(), 2);
4597 assert_eq!(doc.cell_extent(cells[0]).expect("extent"), Some((2, 3)));
4598 assert_eq!(doc.cell_extent(cells[1]).expect("extent"), Some((1, 1)));
4600
4601 let mut pipe =
4603 Document::parse_str("| a |\n| --- |\n| b |\n", Format::Markdown).expect("parse");
4604 let pipe_cell = pipe
4605 .nodes()
4606 .expect("nodes")
4607 .iter()
4608 .find(|n| n.kind == Kind::Cell)
4609 .expect("a cell")
4610 .id;
4611 assert_eq!(pipe.cell_extent(pipe_cell).expect("extent"), Some((1, 1)));
4612
4613 let root = NodeId(0);
4615 assert_eq!(pipe.cell_extent(root).expect("extent"), None);
4616 }
4617
4618 #[test]
4619 fn builder_add_cell_spanning_renders_colspan_and_rowspan() {
4620 let mut b = Builder::new().expect("builder");
4621 let wide_text = b.add_text(TextKind::Str, "wide").expect("str");
4622 let wide = b
4623 .add_cell_spanning(false, Alignment::Default, 2, 3)
4624 .expect("cell");
4625 b.set_children(wide, &[wide_text]).expect("children");
4626 let plain_text = b.add_text(TextKind::Str, "one").expect("str");
4627 let plain = b.add_cell(false, Alignment::Default).expect("cell");
4628 b.set_children(plain, &[plain_text]).expect("children");
4629 let row = b.add_row(false).expect("row");
4630 b.set_children(row, &[wide, plain]).expect("children");
4631 let table = b.add(VoidKind::Table).expect("table");
4632 b.set_children(table, &[row]).expect("children");
4633
4634 let html = String::from_utf8(b.render_html(table).expect("html")).expect("utf-8");
4635 assert!(
4636 html.contains("<td colspan=\"2\" rowspan=\"3\">wide</td>"),
4637 "{html}"
4638 );
4639 assert!(html.contains("<td>one</td>"), "{html}");
4641
4642 assert!(matches!(
4644 b.add_cell_spanning(false, Alignment::Default, 0, 1),
4645 Err(Error::InvalidArgument)
4646 ));
4647 }
4648
4649 #[test]
4650 fn editor_node_at_and_ancestors_hit_test_offsets() {
4651 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4652
4653 let m = ed
4655 .node_at(2)
4656 .expect("node_at")
4657 .expect("a node covers offset 2");
4658 assert!(m.span.contains(&2));
4659
4660 let chain = ed.ancestors_at(2).expect("ancestors_at");
4662 assert!(!chain.is_empty());
4663 assert_eq!(chain[0].kind, Kind::Doc);
4664 assert_eq!(chain.last().unwrap().node_id, m.node_id);
4665
4666 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
4668 }
4669
4670 #[test]
4673 fn editor_wrap_and_toggle_inline_round_trip() {
4674 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4675
4676 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
4678 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
4679 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
4680
4681 ed.toggle_inline(4, 8, InlineKind::Strong)
4683 .expect("toggle off");
4684 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4685
4686 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
4688 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
4689 }
4690
4691 #[test]
4692 fn editor_inline_kind_support_is_format_specific() {
4693 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4695 assert_eq!(
4696 md.wrap_range(2, 6, InlineKind::Mark),
4697 Err(Error::UnsupportedFormat)
4698 );
4699
4700 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4702 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
4703 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
4704 }
4705
4706 #[test]
4707 fn editor_authors_gfm_strikethrough_out_of_the_box() {
4708 assert!(Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Delete)));
4712 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4713 ed.toggle_inline(2, 6, InlineKind::Delete).expect("strike");
4714 assert_eq!(ed.source_str().unwrap(), "a ~~word~~ b\n");
4715 ed.toggle_inline(4, 8, InlineKind::Delete).expect("unstrike");
4716 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4717 }
4718
4719 #[test]
4720 fn editor_highlight_is_authorable_with_the_extension_on() {
4721 let exts = MarkdownExtensions {
4722 highlight: true,
4723 ..Default::default()
4724 };
4725 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
4729 assert!(Format::Markdown.supports_with(exts, Gesture::ToggleInline(InlineKind::Mark)));
4730
4731 let mut ed =
4732 Editor::new_ext(b"a word b\n", Format::Markdown, exts).expect("editor");
4733 ed.toggle_inline(2, 6, InlineKind::Mark).expect("highlight");
4734 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4735 ed.toggle_inline(4, 8, InlineKind::Mark).expect("unhighlight");
4736 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4737 }
4738
4739 #[test]
4740 fn editor_set_mark_color_writes_reads_and_clears_the_colour() {
4741 let exts = MarkdownExtensions {
4742 highlight: true,
4743 highlight_colors: true,
4744 ..Default::default()
4745 };
4746 assert!(Format::Markdown.supports_with(exts, Gesture::SetMarkColor));
4747 let hi_only = MarkdownExtensions {
4749 highlight: true,
4750 ..Default::default()
4751 };
4752 assert!(!Format::Markdown.supports_with(hi_only, Gesture::SetMarkColor));
4753 assert!(!Format::Markdown.supports(Gesture::SetMarkColor));
4754 assert!(!Format::Djot.supports_with(exts, Gesture::SetMarkColor));
4755
4756 let mut ed =
4757 Editor::new_ext("a ==word== b\n".as_bytes(), Format::Markdown, exts).expect("editor");
4758 ed.set_mark_color(6, Some(MarkColor::Red)).expect("colour");
4759 assert_eq!(ed.source_str().unwrap(), "a ==\u{1F534} word== b\n");
4760
4761 let mut doc =
4763 Document::parse_with(ed.source_str().unwrap().as_bytes(), Format::Markdown, exts)
4764 .expect("parse");
4765 assert_eq!(doc.query("mark[data-color=red]").expect("query").len(), 1);
4766
4767 ed.set_mark_color(9, Some(MarkColor::Blue)).expect("recolour");
4768 assert_eq!(ed.source_str().unwrap(), "a ==\u{1F535} word== b\n");
4769 ed.set_mark_color(9, None).expect("clear");
4770 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4771
4772 assert_eq!(
4774 ed.set_mark_color(0, Some(MarkColor::Red)),
4775 Err(Error::NotEditable)
4776 );
4777 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4778
4779 for c in [
4781 MarkColor::Red,
4782 MarkColor::Orange,
4783 MarkColor::Yellow,
4784 MarkColor::Green,
4785 MarkColor::Blue,
4786 MarkColor::Purple,
4787 MarkColor::Brown,
4788 ] {
4789 assert_eq!(MarkColor::from_str(c.as_str()), Some(c));
4790 }
4791 assert_eq!(MarkColor::from_str("pink"), None);
4792 }
4793
4794 #[test]
4795 fn editor_toggle_strips_verbatim_via_content_span() {
4796 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
4797 ed.toggle_inline(2, 8, InlineKind::Verbatim)
4799 .expect("toggle code off");
4800 assert_eq!(ed.source_str().unwrap(), "a code b\n");
4801
4802 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
4805 ed2.toggle_inline(2, 7, InlineKind::Verbatim)
4806 .expect("toggle multi off");
4807 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
4808 }
4809
4810 #[test]
4811 fn editor_set_block_switches_para_and_heading_levels() {
4812 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
4813
4814 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
4816 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
4817
4818 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
4820 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
4821
4822 ed.set_block(2, BlockKind::Paragraph).expect("to para");
4824 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
4825 }
4826
4827 #[test]
4828 fn editor_set_block_rejects_bad_level_and_format() {
4829 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4830 assert_eq!(
4831 md.set_block(0, BlockKind::Heading(9)),
4832 Err(Error::InvalidArgument)
4833 );
4834
4835 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4836 assert_eq!(
4837 xml.set_block(1, BlockKind::Heading(1)),
4838 Err(Error::UnsupportedFormat)
4839 );
4840 }
4841
4842 #[test]
4843 fn editor_toggle_block_container_round_trips() {
4844 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
4845
4846 let c = ed
4847 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
4848 .expect("quote on");
4849 assert_eq!(ed.source_str().unwrap(), "> a\n");
4850 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
4851
4852 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4853 .expect("quote off");
4854 assert_eq!(ed.source_str().unwrap(), "a\n");
4855 }
4856
4857 #[test]
4858 fn editor_toggle_block_container_nests_a_partial_selection() {
4859 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
4860
4861 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4864 .expect("nest");
4865 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
4866
4867 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
4869 .expect("peel");
4870 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
4871 }
4872
4873 #[test]
4874 fn editor_toggle_block_container_numbers_and_converts_lists() {
4875 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
4876
4877 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
4879 .expect("ordered on");
4880 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
4881
4882 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
4884 .expect("convert");
4885 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
4886 }
4887
4888 #[test]
4889 fn editor_toggle_block_container_rejects_unspellable_format() {
4890 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4891 assert_eq!(
4892 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
4893 Err(Error::UnsupportedFormat)
4894 );
4895 }
4896
4897 #[test]
4898 fn editor_insert_link_wraps_and_repoints() {
4899 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4900
4901 ed.insert_link(2, 6, "http://x.dev").expect("link");
4902 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
4903
4904 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
4906 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
4907 }
4908
4909 #[test]
4910 fn editor_insert_link_repoints_an_autolink() {
4911 for format in [Format::Markdown, Format::Djot] {
4916 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
4917 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
4918 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
4919
4920 let nodes = ed.nodes().expect("nodes");
4922 let url = nodes
4923 .iter()
4924 .find(|n| n.kind == Kind::Url)
4925 .expect("still an autolink");
4926 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
4927 assert!(!nodes.iter().any(|n| n.kind == Kind::Link));
4928 }
4929 }
4930
4931 #[test]
4932 fn editor_insert_link_escapes_the_destination() {
4933 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4936 dj.insert_link(0, 1, "a)b").expect("link");
4937 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
4938
4939 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4943 md.insert_link(0, 1, "a b").expect("link");
4944 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
4945
4946 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
4947 dj2.insert_link(0, 1, "a b").expect("link");
4948 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
4949 }
4950
4951 #[test]
4952 fn editor_insert_image_escapes_the_destination_per_format() {
4953 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4956 md.insert_image(0, 1, "my cat.png").expect("image");
4957 assert_eq!(md.source_str().unwrap(), "\n");
4958
4959 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4960 dj.insert_image(0, 1, "my cat.png").expect("image");
4961 assert_eq!(dj.source_str().unwrap(), "\n");
4962
4963 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
4965 paren.insert_image(0, 1, "a)b.png").expect("image");
4966 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
4967 }
4968
4969 #[test]
4970 fn editor_insert_image_keeps_an_empty_alt_empty() {
4971 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4974 ed.insert_image(1, 1, "cat.png").expect("image");
4975 assert_eq!(ed.source_str().unwrap(), "ab\n");
4976 }
4977
4978 #[test]
4979 fn editor_insert_image_rejects_a_newline_destination() {
4980 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
4981 assert_eq!(
4982 ed.insert_image(0, 1, "a\nb.png"),
4983 Err(Error::InvalidArgument)
4984 );
4985
4986 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4987 assert_eq!(
4988 xml.insert_image(3, 5, "x.png"),
4989 Err(Error::UnsupportedFormat)
4990 );
4991 }
4992
4993 #[test]
4994 fn editor_insert_link_rejects_a_newline_destination() {
4995 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
4996 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
4997
4998 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4999 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
5000 }
5001
5002 #[test]
5003 fn editor_insert_literal_keeps_typed_specials_literal() {
5004 for format in [Format::Markdown, Format::Djot] {
5005 let mut ed = Editor::new_str("z\n", format).expect("editor");
5006 ed.insert_literal(0, "*hi*").expect("literal");
5008
5009 let nodes = ed.nodes().expect("nodes");
5011 assert!(
5012 !nodes
5013 .iter()
5014 .any(|n| n.kind == Kind::Emph || n.kind == Kind::Strong)
5015 );
5016 let text: String = nodes
5017 .iter()
5018 .filter(|n| n.kind == Kind::Str)
5019 .filter_map(|n| n.text.clone())
5020 .collect();
5021 assert_eq!(text, "*hi*z");
5022 }
5023 }
5024
5025 #[test]
5026 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
5027 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
5029 ed.insert_literal(1, "# ").expect("literal");
5030 assert_eq!(ed.source_str().unwrap(), "a# z\n");
5031
5032 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
5034 ed2.insert_literal(0, "# ").expect("literal");
5035 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
5036 assert!(
5037 !ed2.nodes()
5038 .expect("nodes")
5039 .iter()
5040 .any(|n| n.kind == Kind::Heading)
5041 );
5042 }
5043
5044 #[test]
5045 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
5046 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5047 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
5048
5049 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5050 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
5051 }
5052
5053 #[test]
5054 fn editor_insert_line_break_splices_in_cell_br() {
5055 let mut ed =
5056 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
5057 ed.insert_line_break(3).expect("line break");
5059 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
5060 let nodes = ed.nodes().expect("nodes");
5062 assert!(nodes.iter().any(|n| n.kind == Kind::HardBreak));
5063 assert!(!nodes.iter().any(|n| n.kind == Kind::RawInline));
5064 }
5065
5066 #[test]
5067 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
5068 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
5070 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
5071
5072 let mut dj = Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
5074 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
5075
5076 let mut ed =
5078 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
5079 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
5080 }
5081
5082 #[test]
5083 fn editor_insert_thematic_break_is_blank_separated_per_format() {
5084 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
5088 md.insert_thematic_break(0).expect("rule");
5089 assert_eq!(md.source_str().unwrap(), "a\n\n---\n");
5090 let nodes = md.nodes().expect("nodes");
5091 assert!(nodes.iter().any(|n| n.kind == Kind::ThematicBreak));
5092 assert!(!nodes.iter().any(|n| n.kind == Kind::Heading));
5093
5094 let mut dj = Editor::new_str("a\n", Format::Djot).expect("editor");
5097 dj.insert_thematic_break(0).expect("rule");
5098 assert_eq!(dj.source_str().unwrap(), "a\n\n* * *\n");
5099
5100 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5101 assert_eq!(xml.insert_thematic_break(3), Err(Error::UnsupportedFormat));
5102 }
5103
5104 #[test]
5105 fn editor_split_block_keeps_both_halves_the_same_kind() {
5106 let mut item = Editor::new_str("- this is a list item\n", Format::Markdown).expect("editor");
5109 item.split_block(10).expect("split");
5110 assert_eq!(item.source_str().unwrap(), "- this is \n- a list item\n");
5111 let nodes = item.nodes().expect("nodes");
5112 assert_eq!(nodes.iter().filter(|n| n.kind == Kind::ListItem).count(), 2);
5113
5114 let mut tail = Editor::new_str("- a\n", Format::Markdown).expect("editor");
5116 tail.split_block(3).expect("split");
5117 assert_eq!(tail.source_str().unwrap(), "- a\n- \n");
5118
5119 let mut para = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5121 para.split_block(1).expect("split");
5122 assert_eq!(para.source_str().unwrap(), "a\n\nb\n");
5123
5124 let mut table =
5126 Editor::new_str("| a | b |\n|---|---|\n| c | d |\n", Format::Markdown).expect("editor");
5127 assert_eq!(table.split_block(3), Err(Error::NotEditable));
5128
5129 let mut empty = Editor::new_str("", Format::Markdown).expect("editor");
5130 assert_eq!(empty.split_block(0), Err(Error::NotFound));
5131 }
5132
5133 #[test]
5134 fn editor_toggle_code_block_round_trips_and_measures_the_fence() {
5135 let mut ed = Editor::new_str("a\n", Format::Markdown).expect("editor");
5136 ed.toggle_code_block(0, 1, Some("zig")).expect("fence");
5137 assert_eq!(ed.source_str().unwrap(), "```zig\na\n```\n");
5138 let nodes = ed.nodes().expect("nodes");
5139 assert!(nodes.iter().any(|n| n.kind == Kind::CodeBlock));
5140
5141 ed.toggle_code_block(0, 0, None).expect("unfence");
5142 assert_eq!(ed.source_str().unwrap(), "a\n");
5143
5144 let mut runs = Editor::new_str("a ``` b\n", Format::Markdown).expect("editor");
5147 runs.toggle_code_block(0, 7, None).expect("fence");
5148 assert_eq!(runs.source_str().unwrap(), "````\na ``` b\n````\n");
5149 }
5150
5151 #[test]
5152 fn editor_toggle_code_block_refuses_inside_a_list_item() {
5153 let mut ed = Editor::new_str("- a\n- b\n", Format::Markdown).expect("editor");
5156 assert_eq!(ed.toggle_code_block(2, 3, None), Err(Error::NotEditable));
5157 assert_eq!(ed.source_str().unwrap(), "- a\n- b\n");
5158 }
5159
5160 #[test]
5161 fn editor_set_code_language_retags_clears_and_refuses() {
5162 let mut ed = Editor::new_str("```zig\na\n```\n", Format::Markdown).expect("editor");
5163 ed.set_code_language(0, Some("rust")).expect("retag");
5164 assert_eq!(ed.source_str().unwrap(), "```rust\na\n```\n");
5165
5166 ed.set_code_language(0, None).expect("clear");
5169 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
5170 ed.set_code_language(0, Some("")).expect("empty");
5171 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
5172
5173 assert_eq!(
5176 ed.set_code_language(0, Some("a b")),
5177 Err(Error::InvalidArgument)
5178 );
5179 let mut dj = Editor::new_str("```\na\n```\n", Format::Djot).expect("editor");
5181 dj.set_code_language(0, Some("a b"))
5182 .expect("djot info string");
5183 assert_eq!(dj.source_str().unwrap(), "```a b\na\n```\n");
5184
5185 let mut para = Editor::new_str("x\n", Format::Markdown).expect("editor");
5186 assert_eq!(para.set_code_language(0, Some("zig")), Err(Error::NotFound));
5187 }
5188
5189 #[test]
5190 fn editor_task_checkbox_gestures() {
5191 let mut ed = Editor::new_str("- a\n", Format::Markdown).expect("editor");
5192
5193 ed.toggle_task_item(2).expect("add box");
5196 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
5197 assert!(
5198 ed.nodes()
5199 .unwrap()
5200 .iter()
5201 .any(|n| n.kind == Kind::TaskListItem)
5202 );
5203
5204 ed.set_task_checked(6, true).expect("tick");
5205 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
5206 ed.set_task_checked(6, true).expect("no-op");
5208 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
5209
5210 ed.toggle_task_checked(6).expect("flip");
5211 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
5212
5213 ed.toggle_task_item(6).expect("remove box");
5214 assert_eq!(ed.source_str().unwrap(), "- a\n");
5215
5216 assert_eq!(ed.set_task_checked(2, true), Err(Error::NotEditable));
5219 let mut para = Editor::new_str("a\n", Format::Markdown).expect("editor");
5221 assert_eq!(para.toggle_task_item(0), Err(Error::NotFound));
5222 }
5223
5224 #[test]
5225 fn editor_insert_footnote_writes_both_halves_as_one_edit() {
5226 for format in [Format::Markdown, Format::Djot] {
5227 let mut ed = Editor::new_str("see\n", format).expect("editor");
5228 ed.insert_footnote(3, "a").expect("footnote");
5229 assert_eq!(ed.source_str().unwrap(), "see[^a]\n\n[^a]: \n");
5230
5231 let nodes = ed.nodes().expect("nodes");
5233 assert!(nodes.iter().any(|n| n.kind == Kind::FootnoteReference));
5234 assert!(nodes.iter().any(|n| n.kind == Kind::Footnote));
5235
5236 ed.undo().expect("undo");
5238 assert_eq!(ed.source_str().unwrap(), "see\n");
5239 }
5240 }
5241
5242 #[test]
5243 fn editor_insert_footnote_reuses_an_existing_definition() {
5244 let mut ed = Editor::new_str("see\n", Format::Markdown).expect("editor");
5245 ed.insert_footnote(3, "a").expect("first");
5246 ed.insert_footnote(7, "a").expect("second reference");
5247 assert_eq!(ed.source_str().unwrap(), "see[^a][^a]\n\n[^a]: \n");
5248 let defs = ed
5249 .nodes()
5250 .unwrap()
5251 .iter()
5252 .filter(|n| n.kind == Kind::Footnote)
5253 .count();
5254 assert_eq!(defs, 1);
5255
5256 assert_eq!(ed.insert_footnote(3, ""), Err(Error::InvalidArgument));
5257 assert_eq!(ed.insert_footnote(3, "a]b"), Err(Error::InvalidArgument));
5258 }
5259
5260 #[test]
5261 fn editor_undo_redo_round_trip() {
5262 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5263 ed.edit_range(5, 5, "!").expect("edit");
5264 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5265
5266 let change = ed.undo().expect("undo ok").expect("something to undo");
5267 assert_eq!(ed.source_str().unwrap(), "hello\n");
5268 assert_eq!(change.new.end, 5);
5269 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
5270
5271 ed.redo().expect("redo ok").expect("something to redo");
5272 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5273 }
5274
5275 #[test]
5276 fn editor_coalesce_folds_a_run() {
5277 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5278 ed.edit_range(0, 0, "a").expect("edit");
5279 ed.edit_range(1, 1, "b").expect("edit");
5280 ed.coalesce_last_undo().expect("coalesce");
5281 assert_eq!(ed.source_str().unwrap(), "ab\n");
5282 ed.undo().expect("undo ok").expect("something to undo");
5284 assert_eq!(ed.source_str().unwrap(), "\n");
5285 assert!(ed.undo().expect("undo ok").is_none());
5286 }
5287
5288 #[test]
5289 fn editor_revision_bumps_per_successful_mutation() {
5290 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
5291 assert_eq!(ed.revision(), 0);
5292 ed.edit_range(1, 1, "y").expect("edit");
5293 assert_eq!(ed.revision(), 1);
5294
5295 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
5297 assert_eq!(xml.revision(), 0);
5298 assert!(xml.replace_content("0", "<b>").is_err());
5299 assert_eq!(xml.revision(), 0);
5300
5301 ed.undo().expect("undo ok").expect("something to undo");
5303 assert_eq!(ed.revision(), 2);
5304 ed.redo().expect("redo ok").expect("something to redo");
5305 assert_eq!(ed.revision(), 3);
5306 }
5307
5308 #[test]
5309 fn editor_dirty_range_tracks_and_clears() {
5310 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
5311 assert_eq!(ed.dirty_range(), None);
5313
5314 ed.edit_range(2, 2, "XY").expect("edit");
5316 assert_eq!(ed.dirty_range(), Some(2..4));
5317
5318 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
5322 assert!(
5323 d.start <= 2 && d.end >= 10,
5324 "range {d:?} must cover both edits"
5325 );
5326
5327 let rev = ed.revision();
5329 ed.clear_dirty();
5330 assert_eq!(ed.dirty_range(), None);
5331 assert_eq!(ed.revision(), rev);
5332
5333 ed.undo().expect("undo ok").expect("something to undo");
5335 assert!(ed.dirty_range().is_some());
5336 }
5337
5338 #[test]
5339 fn editor_caret_blob_follows_undo_and_redo() {
5340 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5341 assert!(ed.caret_blob().unwrap().is_empty());
5342
5343 ed.set_caret_blob(b"before").expect("set caret");
5345 ed.edit_range(5, 5, "!").expect("edit");
5346 assert!(ed.caret_blob().unwrap().is_empty());
5348 ed.set_caret_blob(b"after").expect("set caret");
5349
5350 ed.undo().expect("undo ok").expect("something to undo");
5352 assert_eq!(ed.source_str().unwrap(), "hello\n");
5353 assert_eq!(ed.caret_blob().unwrap(), b"before");
5354
5355 ed.redo().expect("redo ok").expect("something to redo");
5357 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5358 assert_eq!(ed.caret_blob().unwrap(), b"after");
5359 }
5360
5361 #[test]
5362 fn editor_coalesced_run_keeps_the_pre_run_caret() {
5363 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5364 ed.set_caret_blob(b"c0").expect("set caret");
5365 ed.edit_range(0, 0, "a").expect("edit");
5366 ed.set_caret_blob(b"c1").expect("set caret");
5367 ed.edit_range(1, 1, "b").expect("edit");
5368 ed.coalesce_last_undo().expect("coalesce");
5369 ed.set_caret_blob(b"c2").expect("set caret");
5370
5371 ed.undo().expect("undo ok").expect("something to undo");
5373 assert_eq!(ed.source_str().unwrap(), "\n");
5374 assert_eq!(ed.caret_blob().unwrap(), b"c0");
5375 }
5376
5377 #[test]
5378 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
5379 let mut ed = Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
5380 ed.renumber_ordered_lists(0).expect("renumber ok");
5381 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
5382 }
5383
5384 #[test]
5385 fn editor_renumber_ordered_lists_leaves_djot_prose_alone() {
5386 let src = "1. a\n 2. b\n2. c\n";
5389 let mut dj = Editor::new_str(src, Format::Djot).expect("editor");
5390 dj.renumber_ordered_lists(0).expect("renumber ok");
5391 assert_eq!(dj.source_str().unwrap(), src);
5392
5393 let mut md = Editor::new_str(src, Format::Markdown).expect("editor");
5394 md.renumber_ordered_lists(0).expect("renumber ok");
5395 assert_eq!(md.source_str().unwrap(), "1. a\n 1. b\n2. c\n");
5396 }
5397
5398 #[test]
5399 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
5400 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
5401 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
5402 }
5403
5404 #[test]
5405 fn editor_table_insert_row_and_set_alignment() {
5406 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
5407 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
5408 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
5410 ed.source_str().unwrap(),
5411 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
5412 );
5413 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
5415 }
5416
5417 #[test]
5418 fn editor_table_edit_off_a_table_is_not_found() {
5419 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
5420 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
5421 }
5422
5423 #[test]
5424 fn editor_set_block_converts_setext_heading() {
5425 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
5427 ed.set_block(0, BlockKind::Heading(1))
5428 .expect("setext to atx");
5429 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
5430 }
5431
5432 #[test]
5433 fn editor_unwrap_and_smart_delete() {
5434 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
5435 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
5437
5438 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
5439 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
5441 }
5442
5443 #[test]
5444 fn editor_directives_require_the_extension_flag() {
5445 let src = ":::vis{.public}\nhi\n:::\n";
5446 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
5449 assert_eq!(plain.query("directive").expect("query").len(), 0);
5450 let mut ext = Editor::new_ext(
5452 src.as_bytes(),
5453 Format::Markdown,
5454 MarkdownExtensions {
5455 directives: true,
5456 ..Default::default()
5457 },
5458 )
5459 .expect("editor");
5460 assert_eq!(ext.query("directive").expect("query").len(), 1);
5461 }
5462
5463 #[test]
5464 fn document_html_elements_make_embedded_img_queryable() {
5465 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
5466 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
5468 assert_eq!(plain.query("image").expect("query").len(), 0);
5469 let mut ext = Document::parse_str_with(
5471 src,
5472 Format::Markdown,
5473 MarkdownExtensions {
5474 html_elements: true,
5475 ..Default::default()
5476 },
5477 )
5478 .expect("parse");
5479 let images = ext.query("image").expect("query");
5480 assert_eq!(images.len(), 1);
5481 assert_eq!(images[0].kind, Kind::Image);
5482 }
5483
5484 #[test]
5485 fn editor_filter_public_audience_view() {
5486 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
5487 let mut ed = Editor::new_ext(
5488 src.as_bytes(),
5489 Format::Markdown,
5490 MarkdownExtensions {
5491 directives: true,
5492 ..Default::default()
5493 },
5494 )
5495 .expect("editor");
5496 ed.filter(
5498 "directive[name=vis]",
5499 Some("directive[class~=public]"),
5500 true,
5501 )
5502 .expect("filter");
5503 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
5504 }
5505
5506 #[test]
5507 fn editor_filter_rejects_a_malformed_selector() {
5508 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
5509 assert_eq!(
5510 ed.filter("list >", None, false),
5511 Err(Error::InvalidArgument)
5512 );
5513 }
5514
5515 #[test]
5516 fn builder_builds_and_renders_a_document() {
5517 let mut b = Builder::new().expect("builder");
5518
5519 let title = b.add_text(TextKind::Str, "Title").unwrap();
5521 let heading = b.add_heading(1).unwrap();
5522 b.set_children(heading, &[title]).unwrap();
5523
5524 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
5525 let world = b.add_text(TextKind::Str, "world").unwrap();
5526 let emph = b.add(VoidKind::Emph).unwrap();
5527 b.set_children(emph, &[world]).unwrap();
5528 let para = b.add(VoidKind::Para).unwrap();
5529 b.set_children(para, &[hello, emph]).unwrap();
5530
5531 let doc = b.add(VoidKind::Doc).unwrap();
5532 b.set_children(doc, &[heading, para]).unwrap();
5533
5534 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
5535 assert!(html.contains("<h1>Title</h1>"), "{html}");
5536 assert!(html.contains("<em>world</em>"), "{html}");
5537
5538 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5539 assert!(md.contains("# Title"), "{md}");
5540 assert!(md.contains("*world*"), "{md}");
5541
5542 let matches = b.query(doc, "heading").unwrap();
5543 assert_eq!(matches.len(), 1);
5544 assert_eq!(matches[0].kind, Kind::Heading);
5545
5546 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
5547 assert!(json.contains("\"kind\": \"doc\""), "{json}");
5548 }
5549
5550 #[test]
5551 fn builder_element_with_attributes() {
5552 let mut b = Builder::new().expect("builder");
5553 let inner = b.add_text(TextKind::Str, "hi").unwrap();
5554 let el = b.add_element("section").unwrap();
5555 b.set_children(el, &[inner]).unwrap();
5556 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)])
5557 .unwrap();
5558
5559 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
5560 assert!(html.contains("<section"), "{html}");
5561 assert!(html.contains("class=\"note\""), "{html}");
5562 assert!(html.contains("hidden"), "{html}");
5563 }
5564
5565 #[test]
5566 fn builder_lists_round_trip_to_markdown() {
5567 let mut b = Builder::new().expect("builder");
5568
5569 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
5571 let one_para = b.add(VoidKind::Para).unwrap();
5572 b.set_children(one_para, &[one_txt]).unwrap();
5573 let one = b.add(VoidKind::ListItem).unwrap();
5574 b.set_children(one, &[one_para]).unwrap();
5575
5576 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
5577 let two_para = b.add(VoidKind::Para).unwrap();
5578 b.set_children(two_para, &[two_txt]).unwrap();
5579 let two = b.add(VoidKind::ListItem).unwrap();
5580 b.set_children(two, &[two_para]).unwrap();
5581
5582 let list = b
5583 .add_ordered_list(
5584 OrderedNumbering::Decimal,
5585 OrderedDelim::Period,
5586 true,
5587 Some(1),
5588 )
5589 .unwrap();
5590 b.set_children(list, &[one, two]).unwrap();
5591 let doc = b.add(VoidKind::Doc).unwrap();
5592 b.set_children(doc, &[list]).unwrap();
5593
5594 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5595 assert!(md.contains("1. one"), "{md}");
5596 assert!(md.contains("2. two"), "{md}");
5597 }
5598
5599 #[test]
5600 fn builder_rejects_invalid_kind_and_id() {
5601 let b = Builder::new().expect("builder");
5602 let mut id = 0u32;
5606 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
5607 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5608
5609 let mut ptr = std::ptr::null();
5611 let mut len = 0usize;
5612 let status =
5613 unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
5614 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5615 }
5616
5617 fn all_gestures() -> Vec<Gesture> {
5621 let inline = [
5622 InlineKind::Strong,
5623 InlineKind::Emph,
5624 InlineKind::Verbatim,
5625 InlineKind::Mark,
5626 InlineKind::Superscript,
5627 InlineKind::Subscript,
5628 InlineKind::Insert,
5629 InlineKind::Delete,
5630 ];
5631 let mut all: Vec<Gesture> = Vec::new();
5632 for k in inline {
5633 all.push(Gesture::WrapRange(k));
5634 all.push(Gesture::ToggleInline(k));
5635 }
5636 for k in [
5637 BlockContainerKind::BlockQuote,
5638 BlockContainerKind::BulletList,
5639 BlockContainerKind::OrderedList,
5640 ] {
5641 all.push(Gesture::ToggleBlockContainer(k));
5642 }
5643 all.extend([
5644 Gesture::SetMarkColor,
5645 Gesture::SetBlock,
5646 Gesture::InsertThematicBreak,
5647 Gesture::ToggleCodeBlock,
5648 Gesture::SetCodeLanguage,
5649 Gesture::ToggleTaskItem,
5650 Gesture::SetTaskChecked,
5651 Gesture::ToggleTaskChecked,
5652 Gesture::InsertLink,
5653 Gesture::InsertImage,
5654 Gesture::InsertFootnote,
5655 Gesture::InsertLiteral,
5656 Gesture::InsertLineBreak,
5657 Gesture::SplitBlock,
5658 Gesture::RenumberOrderedLists,
5659 Gesture::TableInsertRow,
5660 Gesture::TableDeleteRow,
5661 Gesture::TableInsertColumn,
5662 Gesture::TableDeleteColumn,
5663 Gesture::TableSetAlignment,
5664 Gesture::TableMoveRow,
5665 Gesture::TableMoveColumn,
5666 ]);
5667 all
5668 }
5669
5670 #[test]
5671 fn the_wire_space_ends_where_the_sweep_does() {
5672 let mut codes: Vec<c_int> = all_gestures().iter().map(|g| g.to_c().0).collect();
5678 codes.sort_unstable();
5679 codes.dedup();
5680 assert_eq!(codes, (0..=24).collect::<Vec<c_int>>());
5681
5682 let mut supported = -1;
5683 for code in &codes {
5684 let status = unsafe {
5685 ffi::twig_format_supports(
5686 ffi::TwigFormat::from(Format::Markdown) as c_int,
5687 *code,
5688 0,
5689 &mut supported,
5690 )
5691 };
5692 assert_eq!(Error::from_status(status), Ok(()), "code {code} did not decode");
5693 }
5694 let status = unsafe {
5696 ffi::twig_format_supports(
5697 ffi::TwigFormat::from(Format::Markdown) as c_int,
5698 25,
5699 0,
5700 &mut supported,
5701 )
5702 };
5703 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5704 }
5705
5706 #[test]
5707 fn supports_answers_per_gesture_where_authorable_cannot() {
5708 assert!(Format::Html.is_authorable());
5713 assert!(Format::Html.supports(Gesture::ToggleInline(InlineKind::Strong)));
5714 assert!(!Format::Html.supports(Gesture::SetBlock));
5715 assert!(!Format::Html.supports(Gesture::ToggleBlockContainer(
5716 BlockContainerKind::BlockQuote
5717 )));
5718 assert!(!Format::Html.supports(Gesture::ToggleCodeBlock));
5719 assert!(!Format::Html.supports(Gesture::InsertLiteral));
5720 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5724 assert!(!Format::Html.supports(Gesture::TableSetAlignment));
5725 assert!(!Format::Html.supports(Gesture::SplitBlock));
5726 assert!(!Format::Html.supports(Gesture::RenumberOrderedLists));
5727 assert!(Format::Markdown.supports(Gesture::TableInsertRow));
5728 assert!(Format::Djot.supports(Gesture::SplitBlock));
5729
5730 for fmt in [Format::Xml] {
5733 assert!(!fmt.is_authorable());
5734 for g in all_gestures() {
5735 assert!(!fmt.supports(g), "{fmt:?} claims to spell {g:?}");
5736 }
5737 }
5738 assert!(Format::Asciidoc.is_authorable());
5741 assert!(Format::Asciidoc.supports(Gesture::SetBlock));
5742 assert!(Format::Asciidoc.supports(Gesture::ToggleInline(InlineKind::Mark)));
5743 assert!(!Format::Asciidoc.supports(Gesture::InsertLink));
5744 assert!(!Format::Asciidoc.supports(Gesture::TableInsertRow));
5745
5746 assert!(Format::Djot.supports(Gesture::ToggleInline(InlineKind::Mark)));
5749 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
5750 assert!(Format::Markdown.supports(Gesture::InsertLineBreak));
5751 assert!(!Format::Djot.supports(Gesture::InsertLineBreak));
5752 }
5753
5754 #[test]
5755 fn supports_agrees_with_what_the_editor_then_does() {
5756 for fmt in [Format::Djot, Format::Markdown, Format::Html] {
5761 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5762 let claimed = fmt.supports(Gesture::ToggleInline(InlineKind::Mark));
5763 let observed = ed.toggle_inline(0, 2, InlineKind::Mark);
5764 assert_eq!(
5765 claimed,
5766 !matches!(observed, Err(Error::UnsupportedFormat)),
5767 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5768 );
5769
5770 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5771 let claimed = fmt.supports(Gesture::SetBlock);
5772 let observed = ed.set_block(0, BlockKind::Heading(1));
5773 assert_eq!(
5774 claimed,
5775 !matches!(observed, Err(Error::UnsupportedFormat)),
5776 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5777 );
5778 }
5779
5780 let src = "<table><tr><td>a</td></tr></table>";
5784 let mut ed = Editor::new_str(src, Format::Html).expect("editor");
5785 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5786 assert_eq!(ed.table_insert_row(15, true), Err(Error::UnsupportedFormat));
5787 assert_eq!(ed.renumber_ordered_lists(15), Err(Error::UnsupportedFormat));
5788 assert!(matches!(ed.split_block(15), Err(Error::UnsupportedFormat)));
5789 assert_eq!(ed.source().expect("source"), src.as_bytes());
5790 }
5791
5792 #[test]
5793 fn supports_rides_the_gestures_own_kind_space() {
5794 let (g, k) = Gesture::ToggleBlockContainer(BlockContainerKind::BulletList).to_c();
5799 assert_eq!((g, k), (3, 1));
5800 let (g, k) = Gesture::ToggleInline(InlineKind::Emph).to_c();
5801 assert_eq!((g, k), (1, 1));
5802 assert_eq!(Gesture::InsertLink.to_c(), (10, 0));
5805
5806 let mut out: c_int = 0;
5808 let status = unsafe {
5809 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 10, 3, &mut out)
5810 };
5811 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5812 let status = unsafe {
5813 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 9999, 0, &mut out)
5814 };
5815 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5816 }
5817}