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(
1972 &mut self,
1973 start: usize,
1974 end: usize,
1975 kind: InlineKind,
1976 ) -> Result<Change, Error> {
1977 self.change_op(|ed, out| unsafe {
1978 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
1979 })
1980 }
1981
1982 pub fn toggle_inline(
1990 &mut self,
1991 start: usize,
1992 end: usize,
1993 kind: InlineKind,
1994 ) -> Result<Change, Error> {
1995 self.change_op(|ed, out| unsafe {
1996 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
1997 })
1998 }
1999
2000 pub fn set_mark_color(
2038 &mut self,
2039 offset: usize,
2040 color: Option<MarkColor>,
2041 ) -> Result<Change, Error> {
2042 let name = color.map(MarkColor::as_str);
2043 let (ptr, len, has) = opt_str(name);
2044 self.change_op(|ed, out| unsafe {
2045 ffi::twig_editor_set_mark_color(ed, offset, ptr, len, has, out)
2046 })
2047 }
2048
2049 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
2068 let (block_kind, level) = kind.to_c();
2069 self.change_op(|ed, out| unsafe {
2070 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
2071 })
2072 }
2073
2074 pub fn toggle_block_container(
2097 &mut self,
2098 start: usize,
2099 end: usize,
2100 kind: BlockContainerKind,
2101 ) -> Result<Change, Error> {
2102 self.change_op(|ed, out| unsafe {
2103 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
2104 })
2105 }
2106
2107 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
2126 self.change_op(|ed, out| unsafe {
2127 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
2128 })?;
2129 Ok(())
2130 }
2131
2132 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
2141 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
2142 }
2143
2144 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
2147 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
2148 }
2149
2150 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
2152 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
2153 }
2154
2155 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
2157 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
2158 }
2159
2160 pub fn table_set_alignment(
2162 &mut self,
2163 offset: usize,
2164 alignment: Alignment,
2165 ) -> Result<(), Error> {
2166 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
2167 }
2168
2169 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
2171 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
2172 }
2173
2174 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
2176 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
2177 }
2178
2179 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
2180 self.change_op(|ed, out| unsafe { ffi::twig_editor_table_edit(ed, offset, op, arg, out) })?;
2181 Ok(())
2182 }
2183
2184 pub fn insert_link(
2229 &mut self,
2230 start: usize,
2231 end: usize,
2232 destination: &str,
2233 ) -> Result<Change, Error> {
2234 self.change_op(|ed, out| unsafe {
2235 ffi::twig_editor_insert_link(
2236 ed,
2237 start,
2238 end,
2239 destination.as_ptr(),
2240 destination.len(),
2241 out,
2242 )
2243 })
2244 }
2245
2246 pub fn insert_image(
2267 &mut self,
2268 start: usize,
2269 end: usize,
2270 destination: &str,
2271 ) -> Result<Change, Error> {
2272 self.change_op(|ed, out| unsafe {
2273 ffi::twig_editor_insert_image(
2274 ed,
2275 start,
2276 end,
2277 destination.as_ptr(),
2278 destination.len(),
2279 out,
2280 )
2281 })
2282 }
2283
2284 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
2305 self.change_op(|ed, out| unsafe {
2306 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
2307 })
2308 }
2309
2310 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
2324 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
2325 }
2326
2327 pub fn insert_thematic_break(&mut self, offset: usize) -> Result<Change, Error> {
2346 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_thematic_break(ed, offset, out) })
2347 }
2348
2349 pub fn split_block(&mut self, offset: usize) -> Result<Change, Error> {
2397 self.change_op(|ed, out| unsafe { ffi::twig_editor_split_block(ed, offset, out) })
2398 }
2399
2400 pub fn toggle_code_block(
2432 &mut self,
2433 start: usize,
2434 end: usize,
2435 language: Option<&str>,
2436 ) -> Result<Change, Error> {
2437 let (ptr, len, has) = opt_str(language);
2438 self.change_op(|ed, out| unsafe {
2439 ffi::twig_editor_toggle_code_block(ed, start, end, ptr, len, has, out)
2440 })
2441 }
2442
2443 pub fn set_code_language(
2453 &mut self,
2454 offset: usize,
2455 language: Option<&str>,
2456 ) -> Result<Change, Error> {
2457 let (ptr, len, has) = opt_str(language);
2458 self.change_op(|ed, out| unsafe {
2459 ffi::twig_editor_set_code_language(ed, offset, ptr, len, has, out)
2460 })
2461 }
2462
2463 pub fn toggle_task_item(&mut self, offset: usize) -> Result<Change, Error> {
2474 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_item(ed, offset, out) })
2475 }
2476
2477 pub fn set_task_checked(&mut self, offset: usize, checked: bool) -> Result<(), Error> {
2491 self.change_op(|ed, out| unsafe {
2492 ffi::twig_editor_set_task_checked(ed, offset, checked as c_int, out)
2493 })?;
2494 Ok(())
2495 }
2496
2497 pub fn toggle_task_checked(&mut self, offset: usize) -> Result<Change, Error> {
2502 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_checked(ed, offset, out) })
2503 }
2504
2505 pub fn insert_footnote(&mut self, offset: usize, label: &str) -> Result<Change, Error> {
2524 self.change_op(|ed, out| unsafe {
2525 ffi::twig_editor_insert_footnote(ed, offset, label.as_ptr(), label.len(), out)
2526 })
2527 }
2528
2529 fn change_op(
2532 &mut self,
2533 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
2534 ) -> Result<Change, Error> {
2535 let mut change = ffi::TwigChange {
2536 old_span: ffi::TwigSpan { start: 0, end: 0 },
2537 new_span: ffi::TwigSpan { start: 0, end: 0 },
2538 };
2539 let status = op(self.raw.as_ptr(), &mut change);
2540 Error::from_status(status)?;
2541 Ok(Change::from_ffi(change))
2542 }
2543
2544 fn apply(
2546 &mut self,
2547 locator: &str,
2548 text: &str,
2549 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
2550 ) -> Result<(), Error> {
2551 let status = op(
2552 self.raw.as_ptr(),
2553 locator.as_ptr(),
2554 locator.len(),
2555 text.as_ptr(),
2556 text.len(),
2557 );
2558 Error::from_status(status)
2559 }
2560}
2561
2562impl Drop for Editor {
2563 fn drop(&mut self) {
2564 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
2565 }
2566}
2567
2568fn collect_bytes(
2573 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
2574) -> Result<Vec<u8>, 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 bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2586 Ok(bytes.to_vec())
2587}
2588
2589fn collect_matches(
2592 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
2593) -> Result<Vec<QueryMatch>, 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 matches = unsafe { std::slice::from_raw_parts(ptr, len) };
2605 matches.iter().map(query_match_from_ffi).collect()
2606}
2607
2608fn collect_flat_nodes(
2611 call: impl FnOnce(*mut *const ffi::TwigFlatNode, *mut usize) -> ffi::TwigStatus,
2612) -> Result<Vec<FlatNode>, Error> {
2613 let mut ptr = std::ptr::null();
2614 let mut len = 0usize;
2615 let status = call(&mut ptr, &mut len);
2616 Error::from_status(status)?;
2617 if len == 0 {
2618 return Ok(Vec::new());
2619 }
2620 if ptr.is_null() {
2621 return Err(Error::Internal);
2622 }
2623 let nodes = unsafe { std::slice::from_raw_parts(ptr, len) };
2624 nodes.iter().map(flat_node_from_ffi).collect()
2625}
2626
2627fn empty_ffi_match() -> ffi::TwigQueryMatch {
2629 ffi::TwigQueryMatch {
2630 node_id: 0,
2631 span: ffi::TwigSpan { start: 0, end: 0 },
2632 content_span: ffi::TwigSpan { start: 0, end: 0 },
2633 has_content_span: 0,
2634 kind: std::ptr::null(),
2635 }
2636}
2637
2638fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
2641 Ok(QueryMatch {
2642 node_id: m.node_id,
2643 span: m.span.start..m.span.end,
2644 content_span: if m.has_content_span != 0 {
2645 Some(m.content_span.start..m.content_span.end)
2646 } else {
2647 None
2648 },
2649 kind: Kind::from(borrowed_cstr(m.kind)?.as_str()),
2650 })
2651}
2652
2653fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
2655 let node_id = |v: u32| {
2656 if v == ffi::TWIG_NO_NODE {
2657 None
2658 } else {
2659 Some(NodeId(v))
2660 }
2661 };
2662 Ok(FlatNode {
2663 id: NodeId(n.id),
2664 parent: node_id(n.parent),
2665 first_child: node_id(n.first_child),
2666 next_sibling: node_id(n.next_sibling),
2667 span: n.span.start..n.span.end,
2668 content_span: if n.has_content_span != 0 {
2669 Some(n.content_span.start..n.content_span.end)
2670 } else {
2671 None
2672 },
2673 level: if n.level != 0 { Some(n.level) } else { None },
2674 kind: Kind::from(borrowed_cstr(n.kind)?.as_str()),
2675 text: borrowed_bytes(n.text_ptr, n.text_len),
2676 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
2677 head: match n.head {
2678 ffi::TWIG_HEAD_NONE => None,
2679 v => Some(v != 0),
2680 },
2681 alignment: Alignment::from_c(n.alignment),
2682 name: borrowed_bytes(n.name_ptr, n.name_len),
2683 directive_form: DirectiveForm::from_c(n.directive_form),
2684 origin: ContainerOrigin::from_c(n.container_origin),
2685 marker_span: if n.has_marker_span != 0 {
2686 Some(n.marker_span.start..n.marker_span.end)
2687 } else {
2688 None
2689 },
2690 checked: match n.checked {
2691 ffi::TWIG_TASK_CHECKED_NONE => None,
2692 v => Some(v != 0),
2693 },
2694 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
2695 })
2696}
2697
2698fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
2702 if ptr.is_null() || len == 0 {
2703 return Vec::new();
2704 }
2705 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
2706 kvs.iter()
2707 .map(|kv| {
2708 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
2709 (key, borrowed_bytes(kv.value, kv.value_len))
2710 })
2711 .collect()
2712}
2713
2714fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
2716 if ptr.is_null() {
2717 return Err(Error::Internal);
2718 }
2719 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
2720 .to_str()
2721 .map_err(|_| Error::Internal)?
2722 .to_owned())
2723}
2724
2725fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
2729 if ptr.is_null() {
2730 return None;
2731 }
2732 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2733 Some(String::from_utf8_lossy(bytes).into_owned())
2734}
2735
2736#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
2740pub struct NodeId(pub u32);
2741
2742#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2745pub enum VoidKind {
2746 Doc,
2747 Para,
2748 ThematicBreak,
2749 Section,
2750 Div,
2751 BlockQuote,
2752 DefinitionList,
2753 Table,
2754 ListItem,
2755 DefinitionListItem,
2756 Term,
2757 Definition,
2758 Caption,
2759 SoftBreak,
2760 HardBreak,
2761 NonBreakingSpace,
2762 Emph,
2763 Strong,
2764 Span,
2765 Mark,
2766 Superscript,
2767 Subscript,
2768 Insert,
2769 Delete,
2770 DoubleQuoted,
2771 SingleQuoted,
2772}
2773
2774impl VoidKind {
2775 fn to_c(self) -> c_int {
2776 match self {
2778 VoidKind::Doc => 0,
2779 VoidKind::Para => 1,
2780 VoidKind::ThematicBreak => 3,
2781 VoidKind::Section => 4,
2782 VoidKind::Div => 5,
2783 VoidKind::BlockQuote => 9,
2784 VoidKind::DefinitionList => 13,
2785 VoidKind::Table => 14,
2786 VoidKind::ListItem => 15,
2787 VoidKind::DefinitionListItem => 17,
2788 VoidKind::Term => 18,
2789 VoidKind::Definition => 19,
2790 VoidKind::Caption => 22,
2791 VoidKind::SoftBreak => 26,
2792 VoidKind::HardBreak => 27,
2793 VoidKind::NonBreakingSpace => 28,
2794 VoidKind::Emph => 38,
2795 VoidKind::Strong => 39,
2796 VoidKind::Span => 42,
2797 VoidKind::Mark => 43,
2798 VoidKind::Superscript => 44,
2799 VoidKind::Subscript => 45,
2800 VoidKind::Insert => 46,
2801 VoidKind::Delete => 47,
2802 VoidKind::DoubleQuoted => 48,
2803 VoidKind::SingleQuoted => 49,
2804 }
2805 }
2806}
2807
2808#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2810pub enum TextKind {
2811 Str,
2812 Symb,
2813 Verbatim,
2814 InlineMath,
2815 DisplayMath,
2816 Url,
2817 Email,
2818 FootnoteReference,
2819 CitationReference,
2822 SubstitutionReference,
2824 Comment,
2825 Doctype,
2826 Cdata,
2827}
2828
2829impl TextKind {
2830 fn to_c(self) -> c_int {
2831 match self {
2832 TextKind::Str => 25,
2833 TextKind::Symb => 29,
2834 TextKind::Verbatim => 30,
2835 TextKind::InlineMath => 32,
2836 TextKind::DisplayMath => 33,
2837 TextKind::Url => 34,
2838 TextKind::Email => 35,
2839 TextKind::FootnoteReference => 36,
2840 TextKind::CitationReference => 58,
2841 TextKind::SubstitutionReference => 59,
2842 TextKind::Comment => 52,
2843 TextKind::Doctype => 53,
2844 TextKind::Cdata => 55,
2845 }
2846 }
2847}
2848
2849#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2851pub enum BulletStyle {
2852 Dash,
2853 Plus,
2854 Star,
2855}
2856
2857impl BulletStyle {
2858 fn to_c(self) -> c_int {
2859 match self {
2860 BulletStyle::Dash => 0,
2861 BulletStyle::Plus => 1,
2862 BulletStyle::Star => 2,
2863 }
2864 }
2865}
2866
2867#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2869pub enum OrderedNumbering {
2870 Decimal,
2871 LowerAlpha,
2872 UpperAlpha,
2873 LowerRoman,
2874 UpperRoman,
2875}
2876
2877impl OrderedNumbering {
2878 fn to_c(self) -> c_int {
2879 match self {
2880 OrderedNumbering::Decimal => 0,
2881 OrderedNumbering::LowerAlpha => 1,
2882 OrderedNumbering::UpperAlpha => 2,
2883 OrderedNumbering::LowerRoman => 3,
2884 OrderedNumbering::UpperRoman => 4,
2885 }
2886 }
2887}
2888
2889#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2891pub enum OrderedDelim {
2892 Period,
2893 ParenAfter,
2894 ParenBoth,
2895}
2896
2897impl OrderedDelim {
2898 fn to_c(self) -> c_int {
2899 match self {
2900 OrderedDelim::Period => 0,
2901 OrderedDelim::ParenAfter => 1,
2902 OrderedDelim::ParenBoth => 2,
2903 }
2904 }
2905}
2906
2907#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2910pub enum Alignment {
2911 Default,
2912 Left,
2913 Right,
2914 Center,
2915}
2916
2917impl Alignment {
2918 fn to_c(self) -> c_int {
2919 match self {
2920 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
2921 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
2922 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
2923 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
2924 }
2925 }
2926
2927 fn from_c(v: c_int) -> Option<Self> {
2930 match v {
2931 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
2932 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
2933 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
2934 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
2935 _ => None,
2936 }
2937 }
2938}
2939
2940#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2942pub enum SmartPunctuation {
2943 LeftSingleQuote,
2944 RightSingleQuote,
2945 LeftDoubleQuote,
2946 RightDoubleQuote,
2947 Ellipses,
2948 EmDash,
2949 EnDash,
2950}
2951
2952impl SmartPunctuation {
2953 fn to_c(self) -> c_int {
2954 match self {
2955 SmartPunctuation::LeftSingleQuote => 0,
2956 SmartPunctuation::RightSingleQuote => 1,
2957 SmartPunctuation::LeftDoubleQuote => 2,
2958 SmartPunctuation::RightDoubleQuote => 3,
2959 SmartPunctuation::Ellipses => 4,
2960 SmartPunctuation::EmDash => 5,
2961 SmartPunctuation::EnDash => 6,
2962 }
2963 }
2964}
2965
2966#[derive(Clone, Debug, Eq, PartialEq)]
2981pub struct Warning {
2982 pub fidelity: Fidelity,
2983 pub path: String,
2990 pub kind: Kind,
2993}
2994
2995#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2997#[non_exhaustive]
2998pub enum Fidelity {
2999 Degraded,
3002 Dropped,
3004}
3005
3006impl Fidelity {
3007 fn from_c(v: c_int) -> Self {
3011 match v {
3012 ffi::TWIG_FIDELITY_DROPPED => Fidelity::Dropped,
3013 _ => Fidelity::Degraded,
3014 }
3015 }
3016}
3017
3018#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3019#[non_exhaustive]
3020pub enum ContainerOrigin {
3021 Element,
3023 Directive,
3027}
3028
3029impl ContainerOrigin {
3030 fn from_c(v: c_int) -> Option<Self> {
3033 match v {
3034 ffi::TWIG_CONTAINER_ORIGIN_ELEMENT => Some(ContainerOrigin::Element),
3035 ffi::TWIG_CONTAINER_ORIGIN_DIRECTIVE => Some(ContainerOrigin::Directive),
3036 _ => None,
3037 }
3038 }
3039}
3040
3041#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3043pub enum DirectiveForm {
3044 Text,
3045 Leaf,
3046 Container,
3047}
3048
3049impl DirectiveForm {
3050 fn to_c(self) -> c_int {
3051 match self {
3052 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
3053 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
3054 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
3055 }
3056 }
3057
3058 fn from_c(v: c_int) -> Option<Self> {
3062 match v {
3063 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
3064 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
3065 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
3066 _ => None,
3067 }
3068 }
3069}
3070
3071fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
3075 match s {
3076 Some(x) => (x.as_ptr(), x.len(), 1),
3077 None => (std::ptr::null(), 0, 0),
3078 }
3079}
3080
3081#[derive(Debug)]
3088pub struct Builder {
3089 raw: NonNull<ffi::TwigBuilder>,
3090}
3091
3092impl Builder {
3093 pub fn new() -> Result<Self, Error> {
3095 let mut raw = std::ptr::null_mut();
3096 let status = unsafe { ffi::twig_builder_create(&mut raw) };
3097 Error::from_status(status)?;
3098 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
3099 Ok(Self { raw })
3100 }
3101
3102 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
3105 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
3106 }
3107
3108 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
3110 self.emit(|b, out| unsafe {
3111 ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out)
3112 })
3113 }
3114
3115 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
3117 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
3118 }
3119
3120 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
3122 let (lp, ll, has) = opt_str(lang);
3123 self.emit(|b, out| unsafe {
3124 ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out)
3125 })
3126 }
3127
3128 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
3130 self.emit(|b, out| unsafe {
3131 ffi::twig_builder_add_raw_block(
3132 b,
3133 format.as_ptr(),
3134 format.len(),
3135 text.as_ptr(),
3136 text.len(),
3137 out,
3138 )
3139 })
3140 }
3141
3142 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
3144 self.emit(|b, out| unsafe {
3145 ffi::twig_builder_add_metadata(
3146 b,
3147 lang.as_ptr(),
3148 lang.len(),
3149 text.as_ptr(),
3150 text.len(),
3151 out,
3152 )
3153 })
3154 }
3155
3156 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
3158 self.emit(|b, out| unsafe {
3159 ffi::twig_builder_add_raw_inline(
3160 b,
3161 format.as_ptr(),
3162 format.len(),
3163 text.as_ptr(),
3164 text.len(),
3165 out,
3166 )
3167 })
3168 }
3169
3170 pub fn add_smart_punctuation(
3175 &mut self,
3176 kind: SmartPunctuation,
3177 text: &str,
3178 ) -> Result<NodeId, Error> {
3179 self.emit(|b, out| unsafe {
3180 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
3181 })
3182 }
3183
3184 pub fn add_link(
3187 &mut self,
3188 destination: Option<&str>,
3189 reference: Option<&str>,
3190 ) -> Result<NodeId, Error> {
3191 let (dp, dl, hd) = opt_str(destination);
3192 let (rp, rl, hr) = opt_str(reference);
3193 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
3194 }
3195
3196 pub fn add_image(
3198 &mut self,
3199 destination: Option<&str>,
3200 reference: Option<&str>,
3201 ) -> Result<NodeId, Error> {
3202 let (dp, dl, hd) = opt_str(destination);
3203 let (rp, rl, hr) = opt_str(reference);
3204 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
3205 }
3206
3207 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
3209 self.emit(|b, out| unsafe {
3210 ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out)
3211 })
3212 }
3213
3214 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
3216 self.emit(|b, out| unsafe {
3217 ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out)
3218 })
3219 }
3220
3221 pub fn add_processing_instruction(
3223 &mut self,
3224 target: &str,
3225 data: &str,
3226 ) -> Result<NodeId, Error> {
3227 self.emit(|b, out| unsafe {
3228 ffi::twig_builder_add_processing_instruction(
3229 b,
3230 target.as_ptr(),
3231 target.len(),
3232 data.as_ptr(),
3233 data.len(),
3234 out,
3235 )
3236 })
3237 }
3238
3239 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
3241 self.emit(|b, out| unsafe {
3242 ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out)
3243 })
3244 }
3245
3246 pub fn add_citation(&mut self, label: &str) -> Result<NodeId, Error> {
3251 self.emit(|b, out| unsafe {
3252 ffi::twig_builder_add_citation(b, label.as_ptr(), label.len(), out)
3253 })
3254 }
3255
3256 pub fn add_substitution(&mut self, label: &str) -> Result<NodeId, Error> {
3260 self.emit(|b, out| unsafe {
3261 ffi::twig_builder_add_substitution(b, label.as_ptr(), label.len(), out)
3262 })
3263 }
3264
3265 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
3267 self.emit(|b, out| unsafe {
3268 ffi::twig_builder_add_reference(
3269 b,
3270 label.as_ptr(),
3271 label.len(),
3272 destination.as_ptr(),
3273 destination.len(),
3274 out,
3275 )
3276 })
3277 }
3278
3279 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
3281 self.emit(|b, out| unsafe {
3282 ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out)
3283 })
3284 }
3285
3286 pub fn add_ordered_list(
3288 &mut self,
3289 numbering: OrderedNumbering,
3290 delim: OrderedDelim,
3291 tight: bool,
3292 start: Option<u32>,
3293 ) -> Result<NodeId, Error> {
3294 let (start_val, has_start) = match start {
3295 Some(s) => (s, 1),
3296 None => (0, 0),
3297 };
3298 self.emit(|b, out| unsafe {
3299 ffi::twig_builder_add_ordered_list(
3300 b,
3301 numbering.to_c(),
3302 delim.to_c(),
3303 tight as c_int,
3304 start_val,
3305 has_start,
3306 out,
3307 )
3308 })
3309 }
3310
3311 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
3313 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
3314 }
3315
3316 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
3318 self.emit(|b, out| unsafe {
3319 ffi::twig_builder_add_task_list_item(b, checked as c_int, out)
3320 })
3321 }
3322
3323 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
3325 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
3326 }
3327
3328 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
3330 self.emit(|b, out| unsafe {
3331 ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out)
3332 })
3333 }
3334
3335 pub fn add_cell_spanning(
3340 &mut self,
3341 head: bool,
3342 alignment: Alignment,
3343 colspan: u32,
3344 rowspan: u32,
3345 ) -> Result<NodeId, Error> {
3346 self.emit(|b, out| unsafe {
3347 ffi::twig_builder_add_cell_spanning(
3348 b,
3349 head as c_int,
3350 alignment.to_c(),
3351 colspan,
3352 rowspan,
3353 out,
3354 )
3355 })
3356 }
3357
3358 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
3361 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
3362 let status = unsafe {
3363 ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len())
3364 };
3365 Error::from_status(status)
3366 }
3367
3368 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
3372 let kvs: Vec<ffi::TwigKeyVal> = attrs
3373 .iter()
3374 .map(|(k, v)| ffi::TwigKeyVal {
3375 key: k.as_ptr(),
3376 key_len: k.len(),
3377 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
3378 value_len: v.map_or(0, |s| s.len()),
3379 })
3380 .collect();
3381 let status = unsafe {
3382 ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len())
3383 };
3384 Error::from_status(status)
3385 }
3386
3387 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3390 let raw = self.raw.as_ptr();
3391 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
3392 }
3393
3394 pub fn serialize_to(&mut self, root: NodeId, target: Target) -> Result<Vec<u8>, Error> {
3401 let raw = self.raw.as_ptr();
3402 let ffi_target: ffi::TwigFormat = target.into();
3403 collect_bytes(|ptr, len| unsafe {
3404 ffi::twig_builder_serialize(raw, root.0, ffi_target as i32, ptr, len)
3405 })
3406 }
3407
3408 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
3413 self.serialize_to(root, format.into())
3414 }
3415
3416 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3418 let raw = self.raw.as_ptr();
3419 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
3420 }
3421
3422 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
3425 let raw = self.raw.as_ptr();
3426 collect_matches(|ptr, len| unsafe {
3427 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
3428 })
3429 }
3430
3431 fn emit(
3434 &mut self,
3435 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
3436 ) -> Result<NodeId, Error> {
3437 let mut id: u32 = 0;
3438 let status = call(self.raw.as_ptr(), &mut id);
3439 Error::from_status(status)?;
3440 Ok(NodeId(id))
3441 }
3442}
3443
3444impl Drop for Builder {
3445 fn drop(&mut self) {
3446 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
3447 }
3448}
3449
3450#[cfg(test)]
3451mod tests {
3452 use super::*;
3453
3454 #[test]
3455 fn abi_version_matches() {
3456 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
3460 }
3461
3462 #[test]
3463 fn parses_and_renders_markdown_html() {
3464 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3465 let html = doc.render_html().expect("render html");
3466 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
3467 }
3468
3469 #[test]
3470 fn parses_html_input() {
3471 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
3472 let html = doc.render_html().expect("render html");
3473 assert!(String::from_utf8_lossy(&html).contains("hi"));
3474 }
3475
3476 #[test]
3477 fn parses_renders_and_writes_asciidoc() {
3478 let mut doc = Document::parse_str("= Title\n\nsome *bold* text\n", Format::Asciidoc)
3479 .expect("parse asciidoc");
3480 let html = String::from_utf8_lossy(&doc.render_html().expect("render html")).into_owned();
3481 assert!(html.contains("<h1>Title</h1>"), "got {html:?}");
3482 assert!(html.contains("<strong>bold</strong>"), "got {html:?}");
3483
3484 let back = doc.serialize_to(Target::Asciidoc).expect("serialize asciidoc");
3486 assert_eq!(String::from_utf8_lossy(&back), "= Title\n\nsome *bold* text\n");
3487 let mut md = Document::parse_str("# Title\n\nsome **bold** text\n", Format::Markdown)
3488 .expect("parse markdown");
3489 let converted = md.serialize_to(Target::Asciidoc).expect("convert to asciidoc");
3490 assert_eq!(String::from_utf8_lossy(&converted), "= Title\n\nsome *bold* text\n");
3491 assert_eq!(Target::from(Format::Asciidoc), Target::Asciidoc);
3492 assert_eq!(Target::Asciidoc.as_format(), Some(Format::Asciidoc));
3493 }
3494
3495 #[test]
3496 fn serialize_round_trips_and_cross_converts() {
3497 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3498
3499 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
3500 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
3501
3502 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
3504 }
3505
3506 #[test]
3507 fn serialize_markdown_to_djot() {
3508 let mut doc =
3509 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3510 let djot = doc.serialize(Format::Djot).expect("serialize djot");
3511 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3512 }
3513
3514 #[test]
3515 fn serialize_to_takes_the_output_axis() {
3516 let mut doc =
3517 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3518
3519 let djot = doc.serialize_to(Target::Djot).expect("serialize djot");
3520 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3521
3522 assert_eq!(doc.serialize_to(Target::Xml), Err(Error::UnsupportedFormat));
3525 }
3526
3527 #[test]
3528 fn serialize_and_serialize_to_agree() {
3529 let mut a = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3532 let mut b = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3533 for format in [Format::Markdown, Format::Djot, Format::Html] {
3534 assert_eq!(a.serialize(format), b.serialize_to(Target::from(format)));
3535 }
3536 }
3537
3538 #[test]
3539 fn every_format_is_a_target_that_names_it_back() {
3540 for format in [Format::Djot, Format::Markdown, Format::Xml, Format::Html] {
3543 assert_eq!(Target::from(format).as_format(), Some(format));
3544 }
3545 }
3546
3547 #[test]
3548 fn ast_json_dumps_the_tree() {
3549 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
3550 let json = doc.ast_json().expect("ast json");
3551 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
3552 }
3553
3554 #[test]
3555 fn query_finds_nodes_by_selector() {
3556 let source = "# One\n\n## Two\n";
3557 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3558 let matches = doc.query("heading").expect("query");
3559
3560 assert_eq!(matches.len(), 2);
3561 for m in &matches {
3562 assert_eq!(m.kind, Kind::Heading);
3563 assert!(m.span.start < m.span.end);
3564 }
3565 }
3566
3567 #[test]
3568 fn query_recovers_code_spans() {
3569 let source = "prose `code` more prose\n";
3570 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3571 let matches = doc.query("verbatim").expect("query");
3572
3573 assert_eq!(matches.len(), 1);
3574 assert_eq!(&source[matches[0].span.clone()], "`code`");
3575 }
3576
3577 #[test]
3578 fn document_span_accessors_read_by_node_id() {
3579 let source = "# hi\n\ntext\n";
3580 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3581 let heading = doc.query("heading").expect("query").pop().expect("heading");
3582
3583 assert_eq!(
3584 doc.span(NodeId(heading.node_id)).expect("span"),
3585 heading.span
3586 );
3587 assert_eq!(
3588 doc.content_span(NodeId(heading.node_id))
3589 .expect("content span"),
3590 heading.content_span
3591 );
3592 assert_eq!(doc.span(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3593 }
3594
3595 #[test]
3596 fn document_walks_its_tree_without_an_editor() {
3597 let source = "# hi\n\ntext\n";
3598 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3599
3600 let nodes = doc.nodes().expect("nodes");
3601 assert!(nodes.len() >= 3);
3602 for (i, n) in nodes.iter().enumerate() {
3603 assert_eq!(n.id, NodeId(i as u32));
3604 }
3605
3606 let kids = doc.children(None).expect("children");
3607 assert_eq!(kids.len(), 2);
3608 assert_eq!(kids[0].kind, Kind::Heading);
3609
3610 let sub = doc.subtree(NodeId(kids[0].node_id)).expect("subtree");
3611 assert_eq!(sub[0].id, NodeId(0));
3612 assert_eq!(sub[0].parent, None);
3613 assert_eq!(sub[0].span, kids[0].span);
3614
3615 let hit = doc.node_at(2).expect("node_at").expect("a node at 2");
3616 let chain = doc.ancestors_at(2).expect("ancestors");
3617 assert_eq!(chain.last().expect("deepest").node_id, hit.node_id);
3618 assert_eq!(chain[0].kind, Kind::Doc);
3619
3620 assert_eq!(doc.subtree(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3621 }
3622
3623 #[test]
3624 fn editor_document_view_reads_the_live_tree() {
3625 let mut ed = Editor::new_str("# one\n\ntwo\n", Format::Markdown).expect("editor");
3626
3627 {
3628 let mut view = ed.document().expect("view");
3629 let kids = view.children(None).expect("children");
3630 assert_eq!(kids.len(), 2);
3631 assert_eq!(kids[0].kind, Kind::Heading);
3632 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..5);
3633 assert_eq!(view.render_html(), Err(Error::UnsupportedFormat));
3635 assert_eq!(
3636 view.serialize(Format::Markdown),
3637 Err(Error::UnsupportedFormat)
3638 );
3639 }
3640
3641 ed.replace("0", "# one and a half").expect("replace");
3642 let mut view = ed.document().expect("view");
3643 let kids = view.children(None).expect("children");
3644 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..16);
3645 }
3646
3647 #[test]
3648 fn query_rejects_a_malformed_selector() {
3649 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
3650 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
3651 }
3652
3653 #[test]
3654 fn editor_edits_by_index_path() {
3655 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
3656 ed.replace_content("0.0", "bye").expect("replace_content");
3657 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
3658 }
3659
3660 #[test]
3661 fn flat_nodes_expose_element_name_and_attrs() {
3662 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
3666 let mut ed = Editor::new_ext(
3667 src.as_bytes(),
3668 Format::Markdown,
3669 MarkdownExtensions {
3670 html_elements: true,
3671 ..Default::default()
3672 },
3673 )
3674 .expect("editor");
3675 let nodes = ed.nodes().expect("nodes");
3676
3677 let source = nodes
3678 .iter()
3679 .find(|n| n.name.as_deref() == Some("source"))
3680 .expect("a <source> element node");
3681 assert_eq!(
3682 source.attrs,
3683 vec![
3684 (
3685 "media".to_string(),
3686 Some("(prefers-color-scheme: dark)".to_string())
3687 ),
3688 ("srcset".to_string(), Some("d.svg".to_string())),
3689 ]
3690 );
3691
3692 let img = nodes
3695 .iter()
3696 .find(|n| n.kind == Kind::Image)
3697 .expect("an image node");
3698 assert!(img.name.is_none());
3699 assert_eq!(img.destination.as_deref(), Some("l.svg"));
3700
3701 let picture_kids_str = nodes.iter().find(|n| n.kind == Kind::Str);
3703 if let Some(s) = picture_kids_str {
3704 assert!(s.name.is_none() && s.attrs.is_empty());
3705 }
3706 }
3707
3708 #[test]
3709 fn definitions_finds_what_a_walk_from_the_root_cannot() {
3710 let mut doc = Document::parse_str(
3714 "text[^1] [x][a]\n\n[^1]: note\n\n[a]: /u\n",
3715 Format::Markdown,
3716 )
3717 .expect("parse markdown");
3718
3719 let defs = doc.definitions().expect("definitions");
3720 let mut kinds: Vec<Kind> = defs.iter().map(|m| m.kind.clone()).collect();
3721 kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3722 assert_eq!(kinds, vec![Kind::Footnote, Kind::Reference]);
3723
3724 let all = doc.nodes().expect("nodes");
3727 let root = all
3728 .iter()
3729 .find(|n| n.kind == Kind::Doc)
3730 .expect("a doc root");
3731 let mut reachable = vec![root.id];
3732 let mut i = 0;
3733 while i < reachable.len() {
3734 let n = &all[reachable[i].0 as usize];
3735 let mut c = n.first_child;
3736 while let Some(cid) = c {
3737 reachable.push(cid);
3738 c = all[cid.0 as usize].next_sibling;
3739 }
3740 i += 1;
3741 }
3742 for d in &defs {
3743 assert!(
3744 !reachable.contains(&NodeId(d.node_id)),
3745 "{} should be unreachable from the root",
3746 d.kind
3747 );
3748 }
3749
3750 let mut plain = Document::parse_str("just text\n", Format::Markdown).expect("parse");
3752 assert_eq!(plain.definitions().expect("definitions"), Vec::new());
3753 }
3754
3755 #[test]
3756 fn kind_round_trips_through_its_published_name() {
3757 for k in [
3761 Kind::Doc,
3762 Kind::Para,
3763 Kind::Heading,
3764 Kind::Container,
3765 Kind::TaskListItem,
3766 Kind::Superscript,
3767 Kind::FootnoteReference,
3768 Kind::ProcessingInstruction,
3769 Kind::Cdata,
3770 ] {
3771 assert_eq!(Kind::from(k.as_str()), k, "{k} did not round-trip");
3772 assert!(!k.is_unknown());
3773 }
3774 }
3775
3776 #[test]
3777 fn an_unknown_kind_name_is_carried_rather_than_lost() {
3778 let k = Kind::from("some_future_kind");
3781 assert!(k.is_unknown());
3782 assert_eq!(k.as_str(), "some_future_kind");
3783 assert_eq!(k, Kind::Other("some_future_kind".to_string()));
3784 }
3785
3786 #[test]
3787 fn every_kind_the_library_publishes_has_a_variant() {
3788 let cases: &[(&str, Format, MarkdownExtensions)] = &[
3793 (
3794 "# 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",
3795 Format::Markdown,
3796 MarkdownExtensions {
3797 directives: false,
3798 math: false,
3799 html_elements: false,
3800 highlight: false,
3801 highlight_colors: false,
3802 },
3803 ),
3804 (
3805 "| 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",
3806 Format::Markdown,
3807 MarkdownExtensions::default(),
3808 ),
3809 (
3810 ":::note\nbody\n:::\n\n:role[x]\n\n$a+b$ ==h== ==🔴 r==\n",
3811 Format::Markdown,
3812 MarkdownExtensions {
3813 directives: true,
3814 math: true,
3815 html_elements: false,
3816 highlight: true,
3817 highlight_colors: true,
3818 },
3819 ),
3820 (
3821 "a^b^ c~d~ {=e=} {+f+} {-g-} 'q' \"dq\"\n\n\n\n<https://e.com>\n",
3822 Format::Djot,
3823 MarkdownExtensions::default(),
3824 ),
3825 (
3826 "<!-- c --><!DOCTYPE html><video controls><p>x</p></video>",
3827 Format::Html,
3828 MarkdownExtensions::default(),
3829 ),
3830 ];
3831
3832 let mut unknown: Vec<String> = Vec::new();
3833 let mut seen: Vec<String> = Vec::new();
3834 for (src, format, ext) in cases {
3835 let mut ed = Editor::new_ext(src.as_bytes(), *format, *ext).expect("editor");
3836 for n in ed.nodes().expect("nodes") {
3837 if n.kind.is_unknown() {
3838 unknown.push(n.kind.as_str().to_string());
3839 }
3840 seen.push(n.kind.as_str().to_string());
3841 }
3842 }
3843 unknown.sort();
3844 unknown.dedup();
3845 assert!(unknown.is_empty(), "kinds with no variant: {unknown:?}");
3846
3847 seen.sort();
3850 seen.dedup();
3851 assert!(
3852 seen.len() >= 30,
3853 "only {} distinct kinds reached: {seen:?}",
3854 seen.len()
3855 );
3856 }
3857
3858 #[test]
3859 fn diagnostics_report_what_a_conversion_would_lose() {
3860 let mut doc = Document::parse_str("a^b^ c\n", Format::Djot).expect("parse djot");
3864
3865 let to_md = doc
3866 .diagnostics(Target::Markdown)
3867 .expect("markdown diagnostics");
3868 assert_eq!(
3869 to_md,
3870 vec![Warning {
3871 fidelity: Fidelity::Degraded,
3872 path: "0/1".to_string(),
3873 kind: Kind::Superscript,
3874 }]
3875 );
3876
3877 assert_eq!(
3879 doc.diagnostics(Target::Djot).expect("djot diagnostics"),
3880 Vec::new()
3881 );
3882 }
3883
3884 #[test]
3885 fn diagnostics_separate_a_droppable_node_from_a_degradable_one() {
3886 let mut doc =
3890 Document::parse_str("<p>hi</p><!-- secret -->", Format::Html).expect("parse html");
3891 let warnings = doc.diagnostics(Target::Djot).expect("djot diagnostics");
3892 let comment = warnings
3893 .iter()
3894 .find(|w| w.kind == Kind::Comment)
3895 .expect("a warning about the comment");
3896 assert_eq!(comment.fidelity, Fidelity::Dropped);
3897 }
3898
3899 #[test]
3900 fn diagnostics_refuse_a_target_with_no_serializer() {
3901 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3904 assert_eq!(doc.diagnostics(Target::Xml), Err(Error::UnsupportedFormat));
3905 assert!(doc.diagnostics(Target::Asciidoc).is_ok());
3907 }
3908
3909 #[test]
3910 fn diagnostics_flag_a_header_less_table_and_leave_a_headed_one_alone() {
3911 let mut headed = Document::parse_str(
3916 "<table><tr><th>H</th></tr><tr><td>a</td></tr></table>",
3917 Format::Html,
3918 )
3919 .expect("parse headed table");
3920 assert!(
3921 headed
3922 .diagnostics(Target::Markdown)
3923 .expect("diagnostics")
3924 .iter()
3925 .all(|w| w.kind != Kind::Table)
3926 );
3927
3928 let mut headless = Document::parse_str("<table><tr><td>a</td></tr></table>", Format::Html)
3929 .expect("parse header-less table");
3930 let table_warning = headless
3931 .diagnostics(Target::Markdown)
3932 .expect("diagnostics")
3933 .into_iter()
3934 .find(|w| w.kind == Kind::Table)
3935 .expect("a warning about the table");
3936 assert_eq!(table_warning.fidelity, Fidelity::Degraded);
3937 }
3938
3939 #[test]
3940 fn container_origin_separates_a_div_from_a_div() {
3941 let mut html =
3946 Editor::new("<div>hi</div>\n".as_bytes(), Format::Html).expect("html editor");
3947 let mut md = Editor::new_ext(
3948 ":::div\nhi\n:::\n".as_bytes(),
3949 Format::Markdown,
3950 MarkdownExtensions {
3951 directives: true,
3952 ..Default::default()
3953 },
3954 )
3955 .expect("markdown editor");
3956
3957 let html_nodes = html.nodes().expect("html nodes");
3958 let md_nodes = md.nodes().expect("markdown nodes");
3959 let tag = html_nodes
3960 .iter()
3961 .find(|n| n.name.as_deref() == Some("div"))
3962 .expect("a <div> container");
3963 let directive = md_nodes
3964 .iter()
3965 .find(|n| n.name.as_deref() == Some("div"))
3966 .expect("a :::div container");
3967
3968 assert_eq!(tag.kind, directive.kind);
3970 assert_eq!(tag.name, directive.name);
3971 assert_eq!(tag.directive_form, directive.directive_form);
3972 assert_eq!(tag.directive_form, Some(DirectiveForm::Container));
3973
3974 assert_eq!(tag.origin, Some(ContainerOrigin::Element));
3976 assert_eq!(directive.origin, Some(ContainerOrigin::Directive));
3977 }
3978
3979 fn for_both_formats(src: &str, check: impl Fn(&mut Document, Format)) {
3983 for format in [Format::Markdown, Format::Djot] {
3984 let mut doc = Document::parse(src.as_bytes(), format).expect("parse");
3985 check(&mut doc, format);
3986 }
3987 }
3988
3989 #[test]
3990 fn marker_span_is_what_a_rich_view_hides() {
3991 for_both_formats("> - [x] done\n", |doc, format| {
3992 let nodes = doc.nodes().expect("nodes");
3993 let quote = nodes
3994 .iter()
3995 .find(|n| n.kind == Kind::BlockQuote)
3996 .expect("a block quote");
3997 let item = nodes
3998 .iter()
3999 .find(|n| n.kind == Kind::TaskListItem)
4000 .expect("a task item");
4001
4002 assert_eq!(quote.marker_span, Some(0..2), "{format:?}");
4006 assert_eq!(item.marker_span, Some(2..8), "{format:?}");
4007
4008 assert_eq!(quote.content_span, Some(quote.span.clone()), "{format:?}");
4012
4013 let para = nodes
4015 .iter()
4016 .find(|n| n.kind == Kind::Para)
4017 .expect("a paragraph");
4018 assert_eq!(para.marker_span, None, "{format:?}");
4019 });
4020 }
4021
4022 #[test]
4023 fn attrs_span_locates_the_attribute_block_a_heuristic_had_to_guess_at() {
4024 let src = "{.vis .family}\nheld back\n\nplain\n";
4030 let mut doc = Document::parse(src.as_bytes(), Format::Djot).expect("parse");
4031 let nodes = doc.nodes().expect("nodes");
4032 let paras: Vec<&FlatNode> = nodes.iter().filter(|n| n.kind == Kind::Para).collect();
4033 assert_eq!(paras.len(), 2);
4034
4035 let span = doc
4036 .attrs_span(paras[0].id)
4037 .expect("attrs span")
4038 .expect("the attributed paragraph has one");
4039 assert_eq!(&src[span.clone()], "{.vis .family}");
4040 assert!(span.end <= paras[0].span.start);
4043
4044 assert_eq!(doc.attrs_span(paras[1].id).expect("attrs span"), None);
4047 }
4048
4049 #[test]
4050 fn line_prefix_assembles_every_marker_on_the_line() {
4051 for_both_formats("> - [x] done\n", |doc, format| {
4052 assert_eq!(doc.line_prefix(9).expect("prefix"), Some(0..8), "{format:?}");
4055 });
4056 }
4057
4058 #[test]
4059 fn line_prefix_is_none_on_a_continuation_line() {
4060 for_both_formats("> c\n> d\n", |doc, format| {
4066 assert_eq!(doc.line_prefix(2).expect("prefix"), Some(0..2), "{format:?}");
4067 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
4068 });
4069 }
4070
4071 #[test]
4072 fn a_caret_at_a_blocks_end_is_in_that_block_in_both_formats() {
4073 for_both_formats("a\n\nb\n", |doc, format| {
4079 for offset in [0usize, 1, 3, 4] {
4080 let hit = doc
4081 .node_at_caret(offset)
4082 .expect("caret hit")
4083 .expect("some node");
4084 assert_eq!(hit.kind, Kind::Str, "{format:?} at {offset}");
4085 }
4086 for offset in [2usize, 5] {
4089 let hit = doc
4090 .node_at_caret(offset)
4091 .expect("caret hit")
4092 .expect("some node");
4093 assert_eq!(hit.kind, Kind::Doc, "{format:?} at {offset}");
4094 }
4095 });
4096 }
4097
4098 #[test]
4099 fn the_caret_chain_ends_at_the_node_the_scalar_call_returns() {
4100 for_both_formats("- a\n", |doc, format| {
4101 let hit = doc.node_at_caret(3).expect("hit").expect("some node");
4102 let chain = doc.ancestors_at_caret(3).expect("chain");
4103 assert_eq!(chain.last().map(|m| m.node_id), Some(hit.node_id), "{format:?}");
4104 assert!(
4107 chain.iter().any(|m| m.kind == Kind::ListItem),
4108 "{format:?}: chain should reach the list item"
4109 );
4110 });
4111 }
4112
4113 #[test]
4114 fn continuation_prefix_repeats_a_quote_and_indents_past_an_item() {
4115 for_both_formats("> - a\n", |doc, format| {
4116 assert_eq!(doc.line_prefix(4).expect("prefix"), Some(0..4), "{format:?}");
4121 let cont = doc.continuation_prefix(4).expect("continuation");
4122 assert_eq!(cont.text, "> ", "{format:?}");
4123 assert_eq!(cont.columns, 4, "{format:?}");
4124 });
4125 }
4126
4127 #[test]
4128 fn continuation_prefix_answers_on_a_line_that_opens_nothing() {
4129 for_both_formats("> c\n> d\n", |doc, format| {
4132 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
4133 assert_eq!(
4134 doc.continuation_prefix(6).expect("continuation").text,
4135 "> ",
4136 "{format:?}"
4137 );
4138 });
4139 }
4140
4141 #[test]
4142 fn continuation_prefix_takes_an_ordered_markers_own_width() {
4143 for_both_formats("10. x\n", |doc, format| {
4146 assert_eq!(
4147 doc.continuation_prefix(4).expect("continuation").columns,
4148 4,
4149 "{format:?}"
4150 );
4151 });
4152 for_both_formats("1. x\n", |doc, format| {
4153 assert_eq!(
4154 doc.continuation_prefix(3).expect("continuation").columns,
4155 3,
4156 "{format:?}"
4157 );
4158 });
4159 }
4160
4161 #[test]
4162 fn a_blank_line_keeps_a_quote_alive_and_drops_an_items_indent() {
4163 for_both_formats("> - a\n", |doc, format| {
4164 let blank = doc.blank_line_prefix(4).expect("blank");
4165 assert_eq!(blank.text, ">", "{format:?}");
4168 assert_eq!(blank.columns, 1, "{format:?}");
4169 });
4170 for_both_formats("- a\n", |doc, format| {
4173 assert_eq!(doc.blank_line_prefix(3).expect("blank").text, "", "{format:?}");
4174 });
4175 }
4176
4177 #[test]
4178 fn a_prefix_column_count_is_not_its_byte_length() {
4179 let mut doc = Document::parse("- x
4182".as_bytes(), Format::Markdown).expect("parse");
4183 let cont = doc.continuation_prefix(2).expect("continuation");
4184 assert_eq!(cont.columns, 4);
4185 }
4186
4187 #[test]
4188 fn set_block_opens_a_heading_on_a_blank_line() {
4189 for format in [Format::Markdown, Format::Djot] {
4190 let mut ed = Editor::new("a\n\n".as_bytes(), format).expect("editor");
4191 ed.set_block(3, BlockKind::Heading(2)).expect("set_block");
4192 assert_eq!(ed.source().expect("source"), b"a\n\n## ", "{format:?}");
4193 let nodes = ed.nodes().expect("nodes");
4197 assert!(
4198 nodes.iter().any(|n| n.kind == Kind::Heading),
4199 "{format:?}: should have parsed a heading"
4200 );
4201 }
4202 }
4203
4204 #[test]
4205 fn set_block_refuses_a_blank_line_inside_a_code_block() {
4206 for format in [Format::Markdown, Format::Djot] {
4210 let src = "```\nx\n\ny\n```\n";
4211 let mut ed = Editor::new(src.as_bytes(), format).expect("editor");
4212 let blank = src.find("\n\n").expect("a blank line") + 1;
4213 assert!(
4214 matches!(
4215 ed.set_block(blank, BlockKind::Heading(1)),
4216 Err(Error::NotEditable)
4217 ),
4218 "{format:?}"
4219 );
4220 assert_eq!(ed.source().expect("source"), src.as_bytes(), "{format:?}");
4221 }
4222 }
4223
4224 #[test]
4225 fn task_items_report_their_checkbox_state() {
4226 for_both_formats("- [ ] a\n- [x] b\n- [X] c\n- d\n", |doc, format| {
4230 let nodes = doc.nodes().expect("nodes");
4231 let states: Vec<Option<bool>> = nodes
4232 .iter()
4233 .filter(|n| matches!(n.kind, Kind::TaskListItem | Kind::ListItem))
4234 .map(|n| n.checked)
4235 .collect();
4236 assert_eq!(
4237 states,
4238 vec![Some(false), Some(true), Some(true), None],
4239 "{format:?}"
4240 );
4241
4242 for n in nodes.iter().filter(|n| n.kind == Kind::Para) {
4245 assert_eq!(n.checked, None, "{format:?}");
4246 }
4247 });
4248 }
4249
4250 #[test]
4251 fn an_editor_reaches_the_caret_reads_through_its_document_view() {
4252 let mut ed = Editor::new("- a\n".as_bytes(), Format::Markdown).expect("editor");
4257 let mut view = ed.document().expect("document view");
4258
4259 assert_eq!(view.line_prefix(3).expect("prefix"), Some(0..2));
4260 let hit = view.node_at_caret(3).expect("hit").expect("some node");
4261 assert_eq!(hit.kind, Kind::Str);
4262 }
4263
4264 #[test]
4265 fn container_origin_is_none_for_non_containers() {
4266 let mut ed = Editor::new("# hi\n\npara\n".as_bytes(), Format::Markdown).expect("editor");
4269 for n in ed.nodes().expect("nodes") {
4270 assert_eq!(n.origin, None, "{} should carry no origin", n.kind);
4271 }
4272 }
4273
4274 #[test]
4275 fn flat_nodes_expose_directive_name_and_form() {
4276 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
4282 let mut ed = Editor::new_ext(
4283 src.as_bytes(),
4284 Format::Markdown,
4285 MarkdownExtensions {
4286 directives: true,
4287 ..Default::default()
4288 },
4289 )
4290 .expect("editor");
4291 let nodes = ed.nodes().expect("nodes");
4292
4293 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
4294 .iter()
4295 .filter(|n| n.kind == Kind::Container)
4296 .map(|n| (n.name.as_deref(), n.directive_form))
4297 .collect();
4298 assert_eq!(
4299 forms,
4300 vec![
4301 (Some("note"), Some(DirectiveForm::Container)),
4302 (Some("embed"), Some(DirectiveForm::Leaf)),
4303 (Some("abbr"), Some(DirectiveForm::Text)),
4304 ]
4305 );
4306
4307 let embed = nodes
4310 .iter()
4311 .find(|n| n.name.as_deref() == Some("embed"))
4312 .expect("embed");
4313 assert_eq!(
4314 embed.attrs,
4315 vec![("src".to_string(), Some("demo.html".to_string()))]
4316 );
4317 let para = nodes.iter().find(|n| n.kind == Kind::Para).expect("a para");
4318 assert!(para.directive_form.is_none() && para.name.is_none());
4319 }
4320
4321 #[test]
4322 fn editor_insert_child_and_delete() {
4323 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
4324 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4325 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
4326 ed.delete("0.1").expect("delete");
4327 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
4328 }
4329
4330 #[test]
4331 fn editor_edits_by_selector() {
4332 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4333 ed.replace("heading(\"Two\")", "## Renamed")
4334 .expect("replace");
4335 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
4336 }
4337
4338 #[test]
4339 fn editor_locator_errors_are_distinct() {
4340 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
4341 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
4342 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
4343 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
4344 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
4346 }
4347
4348 #[test]
4349 fn editor_reparse_break_rolls_back() {
4350 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
4351 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
4352 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
4353 }
4354
4355 #[test]
4356 fn editor_leaf_content_is_not_editable() {
4357 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4358 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
4359 }
4360
4361 #[test]
4362 fn editor_query_reflects_current_tree() {
4363 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
4364 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4365 assert_eq!(ed.query("element").expect("query").len(), 3);
4367 let json = ed.ast_json().expect("ast_json");
4368 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
4369 }
4370
4371 #[test]
4374 fn editor_edit_range_types_backspaces_and_reports_change() {
4375 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4376
4377 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
4379 assert_eq!(ed.source_str().unwrap(), "aXb\n");
4380 assert_eq!(c.old, 1..1);
4381 assert_eq!(c.new, 1..2);
4382 assert_eq!(c.delta(), 1);
4383
4384 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
4386 assert_eq!(ed.source_str().unwrap(), "ab\n");
4387 assert_eq!(c2.old, 1..2);
4388 assert_eq!(c2.new, 1..1);
4389 assert_eq!(c2.delta(), -1);
4390 }
4391
4392 #[test]
4393 fn editor_edit_range_rejects_bad_ranges() {
4394 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4395 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"); }
4399
4400 #[test]
4401 fn editor_last_change_reports_locator_ops_too() {
4402 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4403 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed")
4406 .expect("replace");
4407 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
4408 let c = ed.last_change().expect("a change was recorded");
4409 assert_eq!(c.old, 7..13);
4411 assert_eq!(c.new, 7..17);
4412 }
4413
4414 #[test]
4415 fn editor_nodes_is_a_walkable_flat_tree() {
4416 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4417 let nodes = ed.nodes().expect("nodes");
4418 assert!(!nodes.is_empty());
4419
4420 for (i, n) in nodes.iter().enumerate() {
4422 assert_eq!(n.id, NodeId(i as u32));
4423 }
4424 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
4426 assert_eq!(roots.len(), 1);
4427 assert_eq!(roots[0].kind, Kind::Doc);
4428
4429 let heading = nodes
4431 .iter()
4432 .find(|n| n.kind == Kind::Heading)
4433 .expect("a heading");
4434 assert_eq!(heading.level, Some(1));
4435 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
4436
4437 assert_eq!(heading.head, None);
4439 assert_eq!(heading.alignment, None);
4440
4441 for n in nodes.iter().filter(|n| n.parent.is_some()) {
4444 let p = &nodes[n.parent.unwrap().0 as usize];
4445 let mut kid = p.first_child;
4446 let mut seen = false;
4447 while let Some(NodeId(k)) = kid {
4448 if k == n.id.0 {
4449 seen = true;
4450 break;
4451 }
4452 kid = nodes[k as usize].next_sibling;
4453 }
4454 assert!(
4455 seen,
4456 "node {:?} not found among its parent's children",
4457 n.id
4458 );
4459 }
4460 }
4461
4462 #[test]
4463 fn editor_child_spans_and_subtree_agree_with_nodes() {
4464 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
4465 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4466 let all = ed.nodes().expect("nodes");
4467 let doc = all.iter().find(|n| n.kind == Kind::Doc).expect("doc");
4468
4469 let top = ed.child_spans(None).expect("child_spans");
4472 let mut want = Vec::new();
4473 let mut c = doc.first_child;
4474 while let Some(id) = c {
4475 want.push(id);
4476 c = all[id.0 as usize].next_sibling;
4477 }
4478 assert_eq!(top.len(), want.len(), "top-level count");
4479 for (m, id) in top.iter().zip(&want) {
4480 assert_eq!(m.node_id, id.0, "child id");
4481 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
4482 assert_eq!(m.span, all[id.0 as usize].span, "child span");
4483 }
4484 assert!(
4486 src[top[0].span.clone()].starts_with('#'),
4487 "first block is the heading"
4488 );
4489
4490 let list = top
4492 .iter()
4493 .find(|m| {
4494 matches!(
4495 m.kind,
4496 Kind::BulletList | Kind::OrderedList | Kind::TaskList
4497 )
4498 })
4499 .expect("a list");
4500 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
4501 assert_eq!(items.len(), 2);
4502 assert!(
4503 items.iter().all(|m| m.kind == Kind::ListItem),
4504 "items: {items:?}"
4505 );
4506
4507 let para = top
4509 .iter()
4510 .find(|m| m.kind == Kind::Para)
4511 .expect("a para")
4512 .node_id;
4513 let sub = ed.subtree(NodeId(para)).expect("subtree");
4514 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
4515 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
4516 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
4517 assert_eq!(sub[0].kind, Kind::Para);
4518 for (i, n) in sub.iter().enumerate() {
4519 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
4520 for link in [n.parent, n.first_child, n.next_sibling]
4521 .into_iter()
4522 .flatten()
4523 {
4524 assert!(
4525 (link.0 as usize) < sub.len(),
4526 "link {link:?} escapes the subtree"
4527 );
4528 }
4529 }
4530 assert!(
4531 src[sub[0].span.clone()].starts_with("Hello"),
4532 "absolute span: {:?}",
4533 &src[sub[0].span.clone()]
4534 );
4535
4536 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<Kind> {
4538 let mut out = Vec::new();
4539 let mut stack = vec![root];
4540 while let Some(id) = stack.pop() {
4541 let n = &all[id.0 as usize];
4542 out.push(n.kind.clone());
4543 let mut c = n.first_child;
4544 while let Some(cid) = c {
4545 stack.push(cid);
4546 c = all[cid.0 as usize].next_sibling;
4547 }
4548 }
4549 out
4550 }
4551 let mut want_kinds = arena_kinds(&all, NodeId(para));
4552 let mut got_kinds: Vec<Kind> = sub.iter().map(|n| n.kind.clone()).collect();
4553 want_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4557 got_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4558 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
4559
4560 assert!(matches!(
4562 ed.subtree(NodeId(9999)),
4563 Err(Error::InvalidArgument)
4564 ));
4565 }
4566
4567 #[test]
4568 fn flat_nodes_carry_table_head_and_alignment() {
4569 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
4573 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4574 let nodes = ed.nodes().expect("nodes");
4575
4576 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Row).collect();
4577 assert_eq!(rows.len(), 2, "a header row and one body row");
4578 assert_eq!(rows[0].head, Some(true), "first row is the header");
4579 assert_eq!(rows[1].head, Some(false), "second row is a body row");
4580
4581 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Cell).collect();
4582 assert_eq!(cells.len(), 4);
4583 assert_eq!(cells[0].alignment, Some(Alignment::Left));
4585 assert_eq!(cells[1].alignment, Some(Alignment::Right));
4586 assert_eq!(cells[2].alignment, Some(Alignment::Left));
4587 assert_eq!(cells[3].alignment, Some(Alignment::Right));
4588 assert_eq!(cells[0].head, Some(true));
4590 assert_eq!(cells[2].head, Some(false));
4591
4592 let mut plain =
4595 Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
4596 let pnodes = plain.nodes().expect("nodes");
4597 let pcell = pnodes
4598 .iter()
4599 .find(|n| n.kind == Kind::Cell)
4600 .expect("a cell");
4601 assert_eq!(pcell.alignment, Some(Alignment::Default));
4602 }
4603
4604 #[test]
4605 fn cell_extent_reports_merged_cells_and_nothing_else() {
4606 let src = "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td><td>b</td></tr></table>";
4607 let mut doc = Document::parse_str(src, Format::Html).expect("parse");
4608 let cells: Vec<NodeId> = doc
4609 .nodes()
4610 .expect("nodes")
4611 .iter()
4612 .filter(|n| n.kind == Kind::Cell)
4613 .map(|n| n.id)
4614 .collect();
4615 assert_eq!(cells.len(), 2);
4616 assert_eq!(doc.cell_extent(cells[0]).expect("extent"), Some((2, 3)));
4617 assert_eq!(doc.cell_extent(cells[1]).expect("extent"), Some((1, 1)));
4619
4620 let mut pipe =
4622 Document::parse_str("| a |\n| --- |\n| b |\n", Format::Markdown).expect("parse");
4623 let pipe_cell = pipe
4624 .nodes()
4625 .expect("nodes")
4626 .iter()
4627 .find(|n| n.kind == Kind::Cell)
4628 .expect("a cell")
4629 .id;
4630 assert_eq!(pipe.cell_extent(pipe_cell).expect("extent"), Some((1, 1)));
4631
4632 let root = NodeId(0);
4634 assert_eq!(pipe.cell_extent(root).expect("extent"), None);
4635 }
4636
4637 #[test]
4638 fn builder_add_cell_spanning_renders_colspan_and_rowspan() {
4639 let mut b = Builder::new().expect("builder");
4640 let wide_text = b.add_text(TextKind::Str, "wide").expect("str");
4641 let wide = b
4642 .add_cell_spanning(false, Alignment::Default, 2, 3)
4643 .expect("cell");
4644 b.set_children(wide, &[wide_text]).expect("children");
4645 let plain_text = b.add_text(TextKind::Str, "one").expect("str");
4646 let plain = b.add_cell(false, Alignment::Default).expect("cell");
4647 b.set_children(plain, &[plain_text]).expect("children");
4648 let row = b.add_row(false).expect("row");
4649 b.set_children(row, &[wide, plain]).expect("children");
4650 let table = b.add(VoidKind::Table).expect("table");
4651 b.set_children(table, &[row]).expect("children");
4652
4653 let html = String::from_utf8(b.render_html(table).expect("html")).expect("utf-8");
4654 assert!(
4655 html.contains("<td colspan=\"2\" rowspan=\"3\">wide</td>"),
4656 "{html}"
4657 );
4658 assert!(html.contains("<td>one</td>"), "{html}");
4660
4661 assert!(matches!(
4663 b.add_cell_spanning(false, Alignment::Default, 0, 1),
4664 Err(Error::InvalidArgument)
4665 ));
4666 }
4667
4668 #[test]
4669 fn editor_node_at_and_ancestors_hit_test_offsets() {
4670 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4671
4672 let m = ed
4674 .node_at(2)
4675 .expect("node_at")
4676 .expect("a node covers offset 2");
4677 assert!(m.span.contains(&2));
4678
4679 let chain = ed.ancestors_at(2).expect("ancestors_at");
4681 assert!(!chain.is_empty());
4682 assert_eq!(chain[0].kind, Kind::Doc);
4683 assert_eq!(chain.last().unwrap().node_id, m.node_id);
4684
4685 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
4687 }
4688
4689 #[test]
4692 fn editor_wrap_and_toggle_inline_round_trip() {
4693 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4694
4695 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
4697 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
4698 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
4699
4700 ed.toggle_inline(4, 8, InlineKind::Strong)
4702 .expect("toggle off");
4703 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4704
4705 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
4707 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
4708 }
4709
4710 #[test]
4711 fn editor_inline_marks_cut_at_block_boundaries() {
4712 let mut ed = Editor::new_str("one two\n\nthree four\n", Format::Markdown)
4715 .expect("editor");
4716 let c = ed.toggle_inline(0, 19, InlineKind::Strong).expect("toggle on");
4717 assert_eq!(
4718 ed.source_str().unwrap(),
4719 "**one two**\n\n**three four**\n"
4720 );
4721
4722 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**one two**\n\n**three four**");
4725 ed.undo().expect("undo");
4726 assert_eq!(ed.source_str().unwrap(), "one two\n\nthree four\n");
4727
4728 ed.toggle_inline(0, 19, InlineKind::Strong).expect("toggle on");
4731 ed.toggle_inline(0, 27, InlineKind::Strong).expect("toggle off");
4732 assert_eq!(ed.source_str().unwrap(), "one two\n\nthree four\n");
4733
4734 let mut fenced = Editor::new_str("```\nx y\n```\n", Format::Markdown).expect("editor");
4736 assert_eq!(
4737 fenced.toggle_inline(4, 7, InlineKind::Strong),
4738 Err(Error::NotEditable)
4739 );
4740 }
4741
4742 #[test]
4743 fn editor_inline_kind_support_is_format_specific() {
4744 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4746 assert_eq!(
4747 md.wrap_range(2, 6, InlineKind::Mark),
4748 Err(Error::UnsupportedFormat)
4749 );
4750
4751 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4753 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
4754 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
4755 }
4756
4757 #[test]
4758 fn editor_authors_gfm_strikethrough_out_of_the_box() {
4759 assert!(Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Delete)));
4763 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4764 ed.toggle_inline(2, 6, InlineKind::Delete).expect("strike");
4765 assert_eq!(ed.source_str().unwrap(), "a ~~word~~ b\n");
4766 ed.toggle_inline(4, 8, InlineKind::Delete).expect("unstrike");
4767 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4768 }
4769
4770 #[test]
4771 fn editor_highlight_is_authorable_with_the_extension_on() {
4772 let exts = MarkdownExtensions {
4773 highlight: true,
4774 ..Default::default()
4775 };
4776 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
4780 assert!(Format::Markdown.supports_with(exts, Gesture::ToggleInline(InlineKind::Mark)));
4781
4782 let mut ed =
4783 Editor::new_ext(b"a word b\n", Format::Markdown, exts).expect("editor");
4784 ed.toggle_inline(2, 6, InlineKind::Mark).expect("highlight");
4785 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4786 ed.toggle_inline(4, 8, InlineKind::Mark).expect("unhighlight");
4787 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4788 }
4789
4790 #[test]
4791 fn editor_set_mark_color_writes_reads_and_clears_the_colour() {
4792 let exts = MarkdownExtensions {
4793 highlight: true,
4794 highlight_colors: true,
4795 ..Default::default()
4796 };
4797 assert!(Format::Markdown.supports_with(exts, Gesture::SetMarkColor));
4798 let hi_only = MarkdownExtensions {
4800 highlight: true,
4801 ..Default::default()
4802 };
4803 assert!(!Format::Markdown.supports_with(hi_only, Gesture::SetMarkColor));
4804 assert!(!Format::Markdown.supports(Gesture::SetMarkColor));
4805 assert!(!Format::Djot.supports_with(exts, Gesture::SetMarkColor));
4806
4807 let mut ed =
4808 Editor::new_ext("a ==word== b\n".as_bytes(), Format::Markdown, exts).expect("editor");
4809 ed.set_mark_color(6, Some(MarkColor::Red)).expect("colour");
4810 assert_eq!(ed.source_str().unwrap(), "a ==\u{1F534} word== b\n");
4811
4812 let mut doc =
4814 Document::parse_with(ed.source_str().unwrap().as_bytes(), Format::Markdown, exts)
4815 .expect("parse");
4816 assert_eq!(doc.query("mark[data-color=red]").expect("query").len(), 1);
4817
4818 ed.set_mark_color(9, Some(MarkColor::Blue)).expect("recolour");
4819 assert_eq!(ed.source_str().unwrap(), "a ==\u{1F535} word== b\n");
4820 ed.set_mark_color(9, None).expect("clear");
4821 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4822
4823 assert_eq!(
4825 ed.set_mark_color(0, Some(MarkColor::Red)),
4826 Err(Error::NotEditable)
4827 );
4828 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4829
4830 for c in [
4832 MarkColor::Red,
4833 MarkColor::Orange,
4834 MarkColor::Yellow,
4835 MarkColor::Green,
4836 MarkColor::Blue,
4837 MarkColor::Purple,
4838 MarkColor::Brown,
4839 ] {
4840 assert_eq!(MarkColor::from_str(c.as_str()), Some(c));
4841 }
4842 assert_eq!(MarkColor::from_str("pink"), None);
4843 }
4844
4845 #[test]
4846 fn editor_toggle_strips_verbatim_via_content_span() {
4847 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
4848 ed.toggle_inline(2, 8, InlineKind::Verbatim)
4850 .expect("toggle code off");
4851 assert_eq!(ed.source_str().unwrap(), "a code b\n");
4852
4853 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
4856 ed2.toggle_inline(2, 7, InlineKind::Verbatim)
4857 .expect("toggle multi off");
4858 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
4859 }
4860
4861 #[test]
4862 fn editor_set_block_switches_para_and_heading_levels() {
4863 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
4864
4865 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
4867 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
4868
4869 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
4871 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
4872
4873 ed.set_block(2, BlockKind::Paragraph).expect("to para");
4875 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
4876 }
4877
4878 #[test]
4879 fn editor_set_block_rejects_bad_level_and_format() {
4880 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4881 assert_eq!(
4882 md.set_block(0, BlockKind::Heading(9)),
4883 Err(Error::InvalidArgument)
4884 );
4885
4886 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4887 assert_eq!(
4888 xml.set_block(1, BlockKind::Heading(1)),
4889 Err(Error::UnsupportedFormat)
4890 );
4891 }
4892
4893 #[test]
4894 fn editor_toggle_block_container_round_trips() {
4895 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
4896
4897 let c = ed
4898 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
4899 .expect("quote on");
4900 assert_eq!(ed.source_str().unwrap(), "> a\n");
4901 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
4902
4903 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4904 .expect("quote off");
4905 assert_eq!(ed.source_str().unwrap(), "a\n");
4906 }
4907
4908 #[test]
4909 fn editor_toggle_block_container_nests_a_partial_selection() {
4910 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
4911
4912 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4915 .expect("nest");
4916 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
4917
4918 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
4920 .expect("peel");
4921 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
4922 }
4923
4924 #[test]
4925 fn editor_toggle_block_container_numbers_and_converts_lists() {
4926 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
4927
4928 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
4930 .expect("ordered on");
4931 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
4932
4933 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
4935 .expect("convert");
4936 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
4937 }
4938
4939 #[test]
4940 fn editor_toggle_block_container_rejects_unspellable_format() {
4941 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4942 assert_eq!(
4943 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
4944 Err(Error::UnsupportedFormat)
4945 );
4946 }
4947
4948 #[test]
4949 fn editor_insert_link_wraps_and_repoints() {
4950 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4951
4952 ed.insert_link(2, 6, "http://x.dev").expect("link");
4953 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
4954
4955 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
4957 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
4958 }
4959
4960 #[test]
4961 fn editor_insert_link_repoints_an_autolink() {
4962 for format in [Format::Markdown, Format::Djot] {
4967 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
4968 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
4969 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
4970
4971 let nodes = ed.nodes().expect("nodes");
4973 let url = nodes
4974 .iter()
4975 .find(|n| n.kind == Kind::Url)
4976 .expect("still an autolink");
4977 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
4978 assert!(!nodes.iter().any(|n| n.kind == Kind::Link));
4979 }
4980 }
4981
4982 #[test]
4983 fn editor_insert_link_escapes_the_destination() {
4984 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4987 dj.insert_link(0, 1, "a)b").expect("link");
4988 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
4989
4990 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4994 md.insert_link(0, 1, "a b").expect("link");
4995 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
4996
4997 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
4998 dj2.insert_link(0, 1, "a b").expect("link");
4999 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
5000 }
5001
5002 #[test]
5003 fn editor_insert_image_escapes_the_destination_per_format() {
5004 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
5007 md.insert_image(0, 1, "my cat.png").expect("image");
5008 assert_eq!(md.source_str().unwrap(), "\n");
5009
5010 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
5011 dj.insert_image(0, 1, "my cat.png").expect("image");
5012 assert_eq!(dj.source_str().unwrap(), "\n");
5013
5014 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
5016 paren.insert_image(0, 1, "a)b.png").expect("image");
5017 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
5018 }
5019
5020 #[test]
5021 fn editor_insert_image_keeps_an_empty_alt_empty() {
5022 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5025 ed.insert_image(1, 1, "cat.png").expect("image");
5026 assert_eq!(ed.source_str().unwrap(), "ab\n");
5027 }
5028
5029 #[test]
5030 fn editor_insert_image_rejects_a_newline_destination() {
5031 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
5032 assert_eq!(
5033 ed.insert_image(0, 1, "a\nb.png"),
5034 Err(Error::InvalidArgument)
5035 );
5036
5037 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5038 assert_eq!(
5039 xml.insert_image(3, 5, "x.png"),
5040 Err(Error::UnsupportedFormat)
5041 );
5042 }
5043
5044 #[test]
5045 fn editor_insert_link_rejects_a_newline_destination() {
5046 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
5047 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
5048
5049 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5050 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
5051 }
5052
5053 #[test]
5054 fn editor_insert_literal_keeps_typed_specials_literal() {
5055 for format in [Format::Markdown, Format::Djot] {
5056 let mut ed = Editor::new_str("z\n", format).expect("editor");
5057 ed.insert_literal(0, "*hi*").expect("literal");
5059
5060 let nodes = ed.nodes().expect("nodes");
5062 assert!(
5063 !nodes
5064 .iter()
5065 .any(|n| n.kind == Kind::Emph || n.kind == Kind::Strong)
5066 );
5067 let text: String = nodes
5068 .iter()
5069 .filter(|n| n.kind == Kind::Str)
5070 .filter_map(|n| n.text.clone())
5071 .collect();
5072 assert_eq!(text, "*hi*z");
5073 }
5074 }
5075
5076 #[test]
5077 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
5078 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
5080 ed.insert_literal(1, "# ").expect("literal");
5081 assert_eq!(ed.source_str().unwrap(), "a# z\n");
5082
5083 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
5085 ed2.insert_literal(0, "# ").expect("literal");
5086 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
5087 assert!(
5088 !ed2.nodes()
5089 .expect("nodes")
5090 .iter()
5091 .any(|n| n.kind == Kind::Heading)
5092 );
5093 }
5094
5095 #[test]
5096 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
5097 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5098 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
5099
5100 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5101 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
5102 }
5103
5104 #[test]
5105 fn editor_insert_line_break_splices_in_cell_br() {
5106 let mut ed =
5107 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
5108 ed.insert_line_break(3).expect("line break");
5110 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
5111 let nodes = ed.nodes().expect("nodes");
5113 assert!(nodes.iter().any(|n| n.kind == Kind::HardBreak));
5114 assert!(!nodes.iter().any(|n| n.kind == Kind::RawInline));
5115 }
5116
5117 #[test]
5118 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
5119 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
5121 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
5122
5123 let mut dj = Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
5125 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
5126
5127 let mut ed =
5129 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
5130 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
5131 }
5132
5133 #[test]
5134 fn editor_insert_thematic_break_is_blank_separated_per_format() {
5135 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
5139 md.insert_thematic_break(0).expect("rule");
5140 assert_eq!(md.source_str().unwrap(), "a\n\n---\n");
5141 let nodes = md.nodes().expect("nodes");
5142 assert!(nodes.iter().any(|n| n.kind == Kind::ThematicBreak));
5143 assert!(!nodes.iter().any(|n| n.kind == Kind::Heading));
5144
5145 let mut dj = Editor::new_str("a\n", Format::Djot).expect("editor");
5148 dj.insert_thematic_break(0).expect("rule");
5149 assert_eq!(dj.source_str().unwrap(), "a\n\n* * *\n");
5150
5151 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5152 assert_eq!(xml.insert_thematic_break(3), Err(Error::UnsupportedFormat));
5153 }
5154
5155 #[test]
5156 fn editor_split_block_keeps_both_halves_the_same_kind() {
5157 let mut item = Editor::new_str("- this is a list item\n", Format::Markdown).expect("editor");
5160 item.split_block(10).expect("split");
5161 assert_eq!(item.source_str().unwrap(), "- this is \n- a list item\n");
5162 let nodes = item.nodes().expect("nodes");
5163 assert_eq!(nodes.iter().filter(|n| n.kind == Kind::ListItem).count(), 2);
5164
5165 let mut tail = Editor::new_str("- a\n", Format::Markdown).expect("editor");
5167 tail.split_block(3).expect("split");
5168 assert_eq!(tail.source_str().unwrap(), "- a\n- \n");
5169
5170 let mut para = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5172 para.split_block(1).expect("split");
5173 assert_eq!(para.source_str().unwrap(), "a\n\nb\n");
5174
5175 let mut table =
5177 Editor::new_str("| a | b |\n|---|---|\n| c | d |\n", Format::Markdown).expect("editor");
5178 assert_eq!(table.split_block(3), Err(Error::NotEditable));
5179
5180 let mut empty = Editor::new_str("", Format::Markdown).expect("editor");
5181 assert_eq!(empty.split_block(0), Err(Error::NotFound));
5182 }
5183
5184 #[test]
5185 fn editor_toggle_code_block_round_trips_and_measures_the_fence() {
5186 let mut ed = Editor::new_str("a\n", Format::Markdown).expect("editor");
5187 ed.toggle_code_block(0, 1, Some("zig")).expect("fence");
5188 assert_eq!(ed.source_str().unwrap(), "```zig\na\n```\n");
5189 let nodes = ed.nodes().expect("nodes");
5190 assert!(nodes.iter().any(|n| n.kind == Kind::CodeBlock));
5191
5192 ed.toggle_code_block(0, 0, None).expect("unfence");
5193 assert_eq!(ed.source_str().unwrap(), "a\n");
5194
5195 let mut runs = Editor::new_str("a ``` b\n", Format::Markdown).expect("editor");
5198 runs.toggle_code_block(0, 7, None).expect("fence");
5199 assert_eq!(runs.source_str().unwrap(), "````\na ``` b\n````\n");
5200 }
5201
5202 #[test]
5203 fn editor_toggle_code_block_refuses_inside_a_list_item() {
5204 let mut ed = Editor::new_str("- a\n- b\n", Format::Markdown).expect("editor");
5207 assert_eq!(ed.toggle_code_block(2, 3, None), Err(Error::NotEditable));
5208 assert_eq!(ed.source_str().unwrap(), "- a\n- b\n");
5209 }
5210
5211 #[test]
5212 fn editor_set_code_language_retags_clears_and_refuses() {
5213 let mut ed = Editor::new_str("```zig\na\n```\n", Format::Markdown).expect("editor");
5214 ed.set_code_language(0, Some("rust")).expect("retag");
5215 assert_eq!(ed.source_str().unwrap(), "```rust\na\n```\n");
5216
5217 ed.set_code_language(0, None).expect("clear");
5220 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
5221 ed.set_code_language(0, Some("")).expect("empty");
5222 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
5223
5224 assert_eq!(
5227 ed.set_code_language(0, Some("a b")),
5228 Err(Error::InvalidArgument)
5229 );
5230 let mut dj = Editor::new_str("```\na\n```\n", Format::Djot).expect("editor");
5232 dj.set_code_language(0, Some("a b"))
5233 .expect("djot info string");
5234 assert_eq!(dj.source_str().unwrap(), "```a b\na\n```\n");
5235
5236 let mut para = Editor::new_str("x\n", Format::Markdown).expect("editor");
5237 assert_eq!(para.set_code_language(0, Some("zig")), Err(Error::NotFound));
5238 }
5239
5240 #[test]
5241 fn editor_task_checkbox_gestures() {
5242 let mut ed = Editor::new_str("- a\n", Format::Markdown).expect("editor");
5243
5244 ed.toggle_task_item(2).expect("add box");
5247 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
5248 assert!(
5249 ed.nodes()
5250 .unwrap()
5251 .iter()
5252 .any(|n| n.kind == Kind::TaskListItem)
5253 );
5254
5255 ed.set_task_checked(6, true).expect("tick");
5256 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
5257 ed.set_task_checked(6, true).expect("no-op");
5259 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
5260
5261 ed.toggle_task_checked(6).expect("flip");
5262 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
5263
5264 ed.toggle_task_item(6).expect("remove box");
5265 assert_eq!(ed.source_str().unwrap(), "- a\n");
5266
5267 assert_eq!(ed.set_task_checked(2, true), Err(Error::NotEditable));
5270 let mut para = Editor::new_str("a\n", Format::Markdown).expect("editor");
5272 assert_eq!(para.toggle_task_item(0), Err(Error::NotFound));
5273 }
5274
5275 #[test]
5276 fn editor_insert_footnote_writes_both_halves_as_one_edit() {
5277 for format in [Format::Markdown, Format::Djot] {
5278 let mut ed = Editor::new_str("see\n", format).expect("editor");
5279 ed.insert_footnote(3, "a").expect("footnote");
5280 assert_eq!(ed.source_str().unwrap(), "see[^a]\n\n[^a]: \n");
5281
5282 let nodes = ed.nodes().expect("nodes");
5284 assert!(nodes.iter().any(|n| n.kind == Kind::FootnoteReference));
5285 assert!(nodes.iter().any(|n| n.kind == Kind::Footnote));
5286
5287 ed.undo().expect("undo");
5289 assert_eq!(ed.source_str().unwrap(), "see\n");
5290 }
5291 }
5292
5293 #[test]
5294 fn editor_insert_footnote_reuses_an_existing_definition() {
5295 let mut ed = Editor::new_str("see\n", Format::Markdown).expect("editor");
5296 ed.insert_footnote(3, "a").expect("first");
5297 ed.insert_footnote(7, "a").expect("second reference");
5298 assert_eq!(ed.source_str().unwrap(), "see[^a][^a]\n\n[^a]: \n");
5299 let defs = ed
5300 .nodes()
5301 .unwrap()
5302 .iter()
5303 .filter(|n| n.kind == Kind::Footnote)
5304 .count();
5305 assert_eq!(defs, 1);
5306
5307 assert_eq!(ed.insert_footnote(3, ""), Err(Error::InvalidArgument));
5308 assert_eq!(ed.insert_footnote(3, "a]b"), Err(Error::InvalidArgument));
5309 }
5310
5311 #[test]
5312 fn editor_undo_redo_round_trip() {
5313 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5314 ed.edit_range(5, 5, "!").expect("edit");
5315 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5316
5317 let change = ed.undo().expect("undo ok").expect("something to undo");
5318 assert_eq!(ed.source_str().unwrap(), "hello\n");
5319 assert_eq!(change.new.end, 5);
5320 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
5321
5322 ed.redo().expect("redo ok").expect("something to redo");
5323 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5324 }
5325
5326 #[test]
5327 fn editor_coalesce_folds_a_run() {
5328 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5329 ed.edit_range(0, 0, "a").expect("edit");
5330 ed.edit_range(1, 1, "b").expect("edit");
5331 ed.coalesce_last_undo().expect("coalesce");
5332 assert_eq!(ed.source_str().unwrap(), "ab\n");
5333 ed.undo().expect("undo ok").expect("something to undo");
5335 assert_eq!(ed.source_str().unwrap(), "\n");
5336 assert!(ed.undo().expect("undo ok").is_none());
5337 }
5338
5339 #[test]
5340 fn editor_revision_bumps_per_successful_mutation() {
5341 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
5342 assert_eq!(ed.revision(), 0);
5343 ed.edit_range(1, 1, "y").expect("edit");
5344 assert_eq!(ed.revision(), 1);
5345
5346 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
5348 assert_eq!(xml.revision(), 0);
5349 assert!(xml.replace_content("0", "<b>").is_err());
5350 assert_eq!(xml.revision(), 0);
5351
5352 ed.undo().expect("undo ok").expect("something to undo");
5354 assert_eq!(ed.revision(), 2);
5355 ed.redo().expect("redo ok").expect("something to redo");
5356 assert_eq!(ed.revision(), 3);
5357 }
5358
5359 #[test]
5360 fn editor_dirty_range_tracks_and_clears() {
5361 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
5362 assert_eq!(ed.dirty_range(), None);
5364
5365 ed.edit_range(2, 2, "XY").expect("edit");
5367 assert_eq!(ed.dirty_range(), Some(2..4));
5368
5369 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
5373 assert!(
5374 d.start <= 2 && d.end >= 10,
5375 "range {d:?} must cover both edits"
5376 );
5377
5378 let rev = ed.revision();
5380 ed.clear_dirty();
5381 assert_eq!(ed.dirty_range(), None);
5382 assert_eq!(ed.revision(), rev);
5383
5384 ed.undo().expect("undo ok").expect("something to undo");
5386 assert!(ed.dirty_range().is_some());
5387 }
5388
5389 #[test]
5390 fn editor_caret_blob_follows_undo_and_redo() {
5391 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5392 assert!(ed.caret_blob().unwrap().is_empty());
5393
5394 ed.set_caret_blob(b"before").expect("set caret");
5396 ed.edit_range(5, 5, "!").expect("edit");
5397 assert!(ed.caret_blob().unwrap().is_empty());
5399 ed.set_caret_blob(b"after").expect("set caret");
5400
5401 ed.undo().expect("undo ok").expect("something to undo");
5403 assert_eq!(ed.source_str().unwrap(), "hello\n");
5404 assert_eq!(ed.caret_blob().unwrap(), b"before");
5405
5406 ed.redo().expect("redo ok").expect("something to redo");
5408 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5409 assert_eq!(ed.caret_blob().unwrap(), b"after");
5410 }
5411
5412 #[test]
5413 fn editor_coalesced_run_keeps_the_pre_run_caret() {
5414 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5415 ed.set_caret_blob(b"c0").expect("set caret");
5416 ed.edit_range(0, 0, "a").expect("edit");
5417 ed.set_caret_blob(b"c1").expect("set caret");
5418 ed.edit_range(1, 1, "b").expect("edit");
5419 ed.coalesce_last_undo().expect("coalesce");
5420 ed.set_caret_blob(b"c2").expect("set caret");
5421
5422 ed.undo().expect("undo ok").expect("something to undo");
5424 assert_eq!(ed.source_str().unwrap(), "\n");
5425 assert_eq!(ed.caret_blob().unwrap(), b"c0");
5426 }
5427
5428 #[test]
5429 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
5430 let mut ed = Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
5431 ed.renumber_ordered_lists(0).expect("renumber ok");
5432 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
5433 }
5434
5435 #[test]
5436 fn editor_renumber_ordered_lists_leaves_djot_prose_alone() {
5437 let src = "1. a\n 2. b\n2. c\n";
5440 let mut dj = Editor::new_str(src, Format::Djot).expect("editor");
5441 dj.renumber_ordered_lists(0).expect("renumber ok");
5442 assert_eq!(dj.source_str().unwrap(), src);
5443
5444 let mut md = Editor::new_str(src, Format::Markdown).expect("editor");
5445 md.renumber_ordered_lists(0).expect("renumber ok");
5446 assert_eq!(md.source_str().unwrap(), "1. a\n 1. b\n2. c\n");
5447 }
5448
5449 #[test]
5450 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
5451 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
5452 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
5453 }
5454
5455 #[test]
5456 fn editor_table_insert_row_and_set_alignment() {
5457 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
5458 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
5459 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
5461 ed.source_str().unwrap(),
5462 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
5463 );
5464 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
5466 }
5467
5468 #[test]
5469 fn editor_table_edit_off_a_table_is_not_found() {
5470 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
5471 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
5472 }
5473
5474 #[test]
5475 fn editor_set_block_converts_setext_heading() {
5476 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
5478 ed.set_block(0, BlockKind::Heading(1))
5479 .expect("setext to atx");
5480 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
5481 }
5482
5483 #[test]
5484 fn editor_unwrap_and_smart_delete() {
5485 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
5486 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
5488
5489 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
5490 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
5492 }
5493
5494 #[test]
5495 fn editor_directives_require_the_extension_flag() {
5496 let src = ":::vis{.public}\nhi\n:::\n";
5497 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
5500 assert_eq!(plain.query("directive").expect("query").len(), 0);
5501 let mut ext = Editor::new_ext(
5503 src.as_bytes(),
5504 Format::Markdown,
5505 MarkdownExtensions {
5506 directives: true,
5507 ..Default::default()
5508 },
5509 )
5510 .expect("editor");
5511 assert_eq!(ext.query("directive").expect("query").len(), 1);
5512 }
5513
5514 #[test]
5515 fn document_html_elements_make_embedded_img_queryable() {
5516 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
5517 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
5519 assert_eq!(plain.query("image").expect("query").len(), 0);
5520 let mut ext = Document::parse_str_with(
5522 src,
5523 Format::Markdown,
5524 MarkdownExtensions {
5525 html_elements: true,
5526 ..Default::default()
5527 },
5528 )
5529 .expect("parse");
5530 let images = ext.query("image").expect("query");
5531 assert_eq!(images.len(), 1);
5532 assert_eq!(images[0].kind, Kind::Image);
5533 }
5534
5535 #[test]
5536 fn editor_filter_public_audience_view() {
5537 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
5538 let mut ed = Editor::new_ext(
5539 src.as_bytes(),
5540 Format::Markdown,
5541 MarkdownExtensions {
5542 directives: true,
5543 ..Default::default()
5544 },
5545 )
5546 .expect("editor");
5547 ed.filter(
5549 "directive[name=vis]",
5550 Some("directive[class~=public]"),
5551 true,
5552 )
5553 .expect("filter");
5554 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
5555 }
5556
5557 #[test]
5558 fn editor_filter_rejects_a_malformed_selector() {
5559 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
5560 assert_eq!(
5561 ed.filter("list >", None, false),
5562 Err(Error::InvalidArgument)
5563 );
5564 }
5565
5566 #[test]
5567 fn builder_builds_and_renders_a_document() {
5568 let mut b = Builder::new().expect("builder");
5569
5570 let title = b.add_text(TextKind::Str, "Title").unwrap();
5572 let heading = b.add_heading(1).unwrap();
5573 b.set_children(heading, &[title]).unwrap();
5574
5575 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
5576 let world = b.add_text(TextKind::Str, "world").unwrap();
5577 let emph = b.add(VoidKind::Emph).unwrap();
5578 b.set_children(emph, &[world]).unwrap();
5579 let para = b.add(VoidKind::Para).unwrap();
5580 b.set_children(para, &[hello, emph]).unwrap();
5581
5582 let doc = b.add(VoidKind::Doc).unwrap();
5583 b.set_children(doc, &[heading, para]).unwrap();
5584
5585 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
5586 assert!(html.contains("<h1>Title</h1>"), "{html}");
5587 assert!(html.contains("<em>world</em>"), "{html}");
5588
5589 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5590 assert!(md.contains("# Title"), "{md}");
5591 assert!(md.contains("*world*"), "{md}");
5592
5593 let matches = b.query(doc, "heading").unwrap();
5594 assert_eq!(matches.len(), 1);
5595 assert_eq!(matches[0].kind, Kind::Heading);
5596
5597 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
5598 assert!(json.contains("\"kind\": \"doc\""), "{json}");
5599 }
5600
5601 #[test]
5602 fn builder_element_with_attributes() {
5603 let mut b = Builder::new().expect("builder");
5604 let inner = b.add_text(TextKind::Str, "hi").unwrap();
5605 let el = b.add_element("section").unwrap();
5606 b.set_children(el, &[inner]).unwrap();
5607 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)])
5608 .unwrap();
5609
5610 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
5611 assert!(html.contains("<section"), "{html}");
5612 assert!(html.contains("class=\"note\""), "{html}");
5613 assert!(html.contains("hidden"), "{html}");
5614 }
5615
5616 #[test]
5617 fn builder_lists_round_trip_to_markdown() {
5618 let mut b = Builder::new().expect("builder");
5619
5620 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
5622 let one_para = b.add(VoidKind::Para).unwrap();
5623 b.set_children(one_para, &[one_txt]).unwrap();
5624 let one = b.add(VoidKind::ListItem).unwrap();
5625 b.set_children(one, &[one_para]).unwrap();
5626
5627 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
5628 let two_para = b.add(VoidKind::Para).unwrap();
5629 b.set_children(two_para, &[two_txt]).unwrap();
5630 let two = b.add(VoidKind::ListItem).unwrap();
5631 b.set_children(two, &[two_para]).unwrap();
5632
5633 let list = b
5634 .add_ordered_list(
5635 OrderedNumbering::Decimal,
5636 OrderedDelim::Period,
5637 true,
5638 Some(1),
5639 )
5640 .unwrap();
5641 b.set_children(list, &[one, two]).unwrap();
5642 let doc = b.add(VoidKind::Doc).unwrap();
5643 b.set_children(doc, &[list]).unwrap();
5644
5645 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5646 assert!(md.contains("1. one"), "{md}");
5647 assert!(md.contains("2. two"), "{md}");
5648 }
5649
5650 #[test]
5651 fn builder_rejects_invalid_kind_and_id() {
5652 let b = Builder::new().expect("builder");
5653 let mut id = 0u32;
5657 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
5658 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5659
5660 let mut ptr = std::ptr::null();
5662 let mut len = 0usize;
5663 let status =
5664 unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
5665 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5666 }
5667
5668 fn all_gestures() -> Vec<Gesture> {
5672 let inline = [
5673 InlineKind::Strong,
5674 InlineKind::Emph,
5675 InlineKind::Verbatim,
5676 InlineKind::Mark,
5677 InlineKind::Superscript,
5678 InlineKind::Subscript,
5679 InlineKind::Insert,
5680 InlineKind::Delete,
5681 ];
5682 let mut all: Vec<Gesture> = Vec::new();
5683 for k in inline {
5684 all.push(Gesture::WrapRange(k));
5685 all.push(Gesture::ToggleInline(k));
5686 }
5687 for k in [
5688 BlockContainerKind::BlockQuote,
5689 BlockContainerKind::BulletList,
5690 BlockContainerKind::OrderedList,
5691 ] {
5692 all.push(Gesture::ToggleBlockContainer(k));
5693 }
5694 all.extend([
5695 Gesture::SetMarkColor,
5696 Gesture::SetBlock,
5697 Gesture::InsertThematicBreak,
5698 Gesture::ToggleCodeBlock,
5699 Gesture::SetCodeLanguage,
5700 Gesture::ToggleTaskItem,
5701 Gesture::SetTaskChecked,
5702 Gesture::ToggleTaskChecked,
5703 Gesture::InsertLink,
5704 Gesture::InsertImage,
5705 Gesture::InsertFootnote,
5706 Gesture::InsertLiteral,
5707 Gesture::InsertLineBreak,
5708 Gesture::SplitBlock,
5709 Gesture::RenumberOrderedLists,
5710 Gesture::TableInsertRow,
5711 Gesture::TableDeleteRow,
5712 Gesture::TableInsertColumn,
5713 Gesture::TableDeleteColumn,
5714 Gesture::TableSetAlignment,
5715 Gesture::TableMoveRow,
5716 Gesture::TableMoveColumn,
5717 ]);
5718 all
5719 }
5720
5721 #[test]
5722 fn the_wire_space_ends_where_the_sweep_does() {
5723 let mut codes: Vec<c_int> = all_gestures().iter().map(|g| g.to_c().0).collect();
5729 codes.sort_unstable();
5730 codes.dedup();
5731 assert_eq!(codes, (0..=24).collect::<Vec<c_int>>());
5732
5733 let mut supported = -1;
5734 for code in &codes {
5735 let status = unsafe {
5736 ffi::twig_format_supports(
5737 ffi::TwigFormat::from(Format::Markdown) as c_int,
5738 *code,
5739 0,
5740 &mut supported,
5741 )
5742 };
5743 assert_eq!(Error::from_status(status), Ok(()), "code {code} did not decode");
5744 }
5745 let status = unsafe {
5747 ffi::twig_format_supports(
5748 ffi::TwigFormat::from(Format::Markdown) as c_int,
5749 25,
5750 0,
5751 &mut supported,
5752 )
5753 };
5754 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5755 }
5756
5757 #[test]
5758 fn supports_answers_per_gesture_where_authorable_cannot() {
5759 assert!(Format::Html.is_authorable());
5764 assert!(Format::Html.supports(Gesture::ToggleInline(InlineKind::Strong)));
5765 assert!(!Format::Html.supports(Gesture::SetBlock));
5766 assert!(!Format::Html.supports(Gesture::ToggleBlockContainer(
5767 BlockContainerKind::BlockQuote
5768 )));
5769 assert!(!Format::Html.supports(Gesture::ToggleCodeBlock));
5770 assert!(!Format::Html.supports(Gesture::InsertLiteral));
5771 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5775 assert!(!Format::Html.supports(Gesture::TableSetAlignment));
5776 assert!(!Format::Html.supports(Gesture::SplitBlock));
5777 assert!(!Format::Html.supports(Gesture::RenumberOrderedLists));
5778 assert!(Format::Markdown.supports(Gesture::TableInsertRow));
5779 assert!(Format::Djot.supports(Gesture::SplitBlock));
5780
5781 for fmt in [Format::Xml] {
5784 assert!(!fmt.is_authorable());
5785 for g in all_gestures() {
5786 assert!(!fmt.supports(g), "{fmt:?} claims to spell {g:?}");
5787 }
5788 }
5789 assert!(Format::Asciidoc.is_authorable());
5792 assert!(Format::Asciidoc.supports(Gesture::SetBlock));
5793 assert!(Format::Asciidoc.supports(Gesture::ToggleInline(InlineKind::Mark)));
5794 assert!(!Format::Asciidoc.supports(Gesture::InsertLink));
5795 assert!(!Format::Asciidoc.supports(Gesture::TableInsertRow));
5796
5797 assert!(Format::Djot.supports(Gesture::ToggleInline(InlineKind::Mark)));
5800 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
5801 assert!(Format::Markdown.supports(Gesture::InsertLineBreak));
5802 assert!(!Format::Djot.supports(Gesture::InsertLineBreak));
5803 }
5804
5805 #[test]
5806 fn supports_agrees_with_what_the_editor_then_does() {
5807 for fmt in [Format::Djot, Format::Markdown, Format::Html] {
5812 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5813 let claimed = fmt.supports(Gesture::ToggleInline(InlineKind::Mark));
5814 let observed = ed.toggle_inline(0, 2, InlineKind::Mark);
5815 assert_eq!(
5816 claimed,
5817 !matches!(observed, Err(Error::UnsupportedFormat)),
5818 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5819 );
5820
5821 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5822 let claimed = fmt.supports(Gesture::SetBlock);
5823 let observed = ed.set_block(0, BlockKind::Heading(1));
5824 assert_eq!(
5825 claimed,
5826 !matches!(observed, Err(Error::UnsupportedFormat)),
5827 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5828 );
5829 }
5830
5831 let src = "<table><tr><td>a</td></tr></table>";
5835 let mut ed = Editor::new_str(src, Format::Html).expect("editor");
5836 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5837 assert_eq!(ed.table_insert_row(15, true), Err(Error::UnsupportedFormat));
5838 assert_eq!(ed.renumber_ordered_lists(15), Err(Error::UnsupportedFormat));
5839 assert!(matches!(ed.split_block(15), Err(Error::UnsupportedFormat)));
5840 assert_eq!(ed.source().expect("source"), src.as_bytes());
5841 }
5842
5843 #[test]
5844 fn supports_rides_the_gestures_own_kind_space() {
5845 let (g, k) = Gesture::ToggleBlockContainer(BlockContainerKind::BulletList).to_c();
5850 assert_eq!((g, k), (3, 1));
5851 let (g, k) = Gesture::ToggleInline(InlineKind::Emph).to_c();
5852 assert_eq!((g, k), (1, 1));
5853 assert_eq!(Gesture::InsertLink.to_c(), (10, 0));
5856
5857 let mut out: c_int = 0;
5859 let status = unsafe {
5860 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 10, 3, &mut out)
5861 };
5862 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5863 let status = unsafe {
5864 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 9999, 0, &mut out)
5865 };
5866 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5867 }
5868}