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 for d in &defs {
3728 let want = match d.kind {
3729 Kind::Footnote => 17..27,
3730 Kind::Reference => 29..36,
3731 _ => unreachable!(),
3732 };
3733 assert_eq!(d.span, want, "{} stands on its own bytes", d.kind);
3734 }
3735
3736 let all = doc.nodes().expect("nodes");
3739 let root = all
3740 .iter()
3741 .find(|n| n.kind == Kind::Doc)
3742 .expect("a doc root");
3743 let mut reachable = vec![root.id];
3744 let mut i = 0;
3745 while i < reachable.len() {
3746 let n = &all[reachable[i].0 as usize];
3747 let mut c = n.first_child;
3748 while let Some(cid) = c {
3749 reachable.push(cid);
3750 c = all[cid.0 as usize].next_sibling;
3751 }
3752 i += 1;
3753 }
3754 for d in &defs {
3755 assert!(
3756 !reachable.contains(&NodeId(d.node_id)),
3757 "{} should be unreachable from the root",
3758 d.kind
3759 );
3760 }
3761
3762 let mut plain = Document::parse_str("just text\n", Format::Markdown).expect("parse");
3764 assert_eq!(plain.definitions().expect("definitions"), Vec::new());
3765 }
3766
3767 #[test]
3768 fn kind_round_trips_through_its_published_name() {
3769 for k in [
3773 Kind::Doc,
3774 Kind::Para,
3775 Kind::Heading,
3776 Kind::Container,
3777 Kind::TaskListItem,
3778 Kind::Superscript,
3779 Kind::FootnoteReference,
3780 Kind::ProcessingInstruction,
3781 Kind::Cdata,
3782 ] {
3783 assert_eq!(Kind::from(k.as_str()), k, "{k} did not round-trip");
3784 assert!(!k.is_unknown());
3785 }
3786 }
3787
3788 #[test]
3789 fn an_unknown_kind_name_is_carried_rather_than_lost() {
3790 let k = Kind::from("some_future_kind");
3793 assert!(k.is_unknown());
3794 assert_eq!(k.as_str(), "some_future_kind");
3795 assert_eq!(k, Kind::Other("some_future_kind".to_string()));
3796 }
3797
3798 #[test]
3799 fn every_kind_the_library_publishes_has_a_variant() {
3800 let cases: &[(&str, Format, MarkdownExtensions)] = &[
3805 (
3806 "# 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",
3807 Format::Markdown,
3808 MarkdownExtensions {
3809 directives: false,
3810 math: false,
3811 html_elements: false,
3812 highlight: false,
3813 highlight_colors: false,
3814 },
3815 ),
3816 (
3817 "| 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",
3818 Format::Markdown,
3819 MarkdownExtensions::default(),
3820 ),
3821 (
3822 ":::note\nbody\n:::\n\n:role[x]\n\n$a+b$ ==h== ==🔴 r==\n",
3823 Format::Markdown,
3824 MarkdownExtensions {
3825 directives: true,
3826 math: true,
3827 html_elements: false,
3828 highlight: true,
3829 highlight_colors: true,
3830 },
3831 ),
3832 (
3833 "a^b^ c~d~ {=e=} {+f+} {-g-} 'q' \"dq\"\n\n\n\n<https://e.com>\n",
3834 Format::Djot,
3835 MarkdownExtensions::default(),
3836 ),
3837 (
3838 "<!-- c --><!DOCTYPE html><video controls><p>x</p></video>",
3839 Format::Html,
3840 MarkdownExtensions::default(),
3841 ),
3842 ];
3843
3844 let mut unknown: Vec<String> = Vec::new();
3845 let mut seen: Vec<String> = Vec::new();
3846 for (src, format, ext) in cases {
3847 let mut ed = Editor::new_ext(src.as_bytes(), *format, *ext).expect("editor");
3848 for n in ed.nodes().expect("nodes") {
3849 if n.kind.is_unknown() {
3850 unknown.push(n.kind.as_str().to_string());
3851 }
3852 seen.push(n.kind.as_str().to_string());
3853 }
3854 }
3855 unknown.sort();
3856 unknown.dedup();
3857 assert!(unknown.is_empty(), "kinds with no variant: {unknown:?}");
3858
3859 seen.sort();
3862 seen.dedup();
3863 assert!(
3864 seen.len() >= 30,
3865 "only {} distinct kinds reached: {seen:?}",
3866 seen.len()
3867 );
3868 }
3869
3870 #[test]
3871 fn diagnostics_report_what_a_conversion_would_lose() {
3872 let mut doc = Document::parse_str("a^b^ c\n", Format::Djot).expect("parse djot");
3876
3877 let to_md = doc
3878 .diagnostics(Target::Markdown)
3879 .expect("markdown diagnostics");
3880 assert_eq!(
3881 to_md,
3882 vec![Warning {
3883 fidelity: Fidelity::Degraded,
3884 path: "0/1".to_string(),
3885 kind: Kind::Superscript,
3886 }]
3887 );
3888
3889 assert_eq!(
3891 doc.diagnostics(Target::Djot).expect("djot diagnostics"),
3892 Vec::new()
3893 );
3894 }
3895
3896 #[test]
3897 fn diagnostics_separate_a_droppable_node_from_a_degradable_one() {
3898 let mut doc =
3902 Document::parse_str("<p>hi</p><!-- secret -->", Format::Html).expect("parse html");
3903 let warnings = doc.diagnostics(Target::Djot).expect("djot diagnostics");
3904 let comment = warnings
3905 .iter()
3906 .find(|w| w.kind == Kind::Comment)
3907 .expect("a warning about the comment");
3908 assert_eq!(comment.fidelity, Fidelity::Dropped);
3909 }
3910
3911 #[test]
3912 fn diagnostics_refuse_a_target_with_no_serializer() {
3913 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3916 assert_eq!(doc.diagnostics(Target::Xml), Err(Error::UnsupportedFormat));
3917 assert!(doc.diagnostics(Target::Asciidoc).is_ok());
3919 }
3920
3921 #[test]
3922 fn diagnostics_flag_a_header_less_table_and_leave_a_headed_one_alone() {
3923 let mut headed = Document::parse_str(
3928 "<table><tr><th>H</th></tr><tr><td>a</td></tr></table>",
3929 Format::Html,
3930 )
3931 .expect("parse headed table");
3932 assert!(
3933 headed
3934 .diagnostics(Target::Markdown)
3935 .expect("diagnostics")
3936 .iter()
3937 .all(|w| w.kind != Kind::Table)
3938 );
3939
3940 let mut headless = Document::parse_str("<table><tr><td>a</td></tr></table>", Format::Html)
3941 .expect("parse header-less table");
3942 let table_warning = headless
3943 .diagnostics(Target::Markdown)
3944 .expect("diagnostics")
3945 .into_iter()
3946 .find(|w| w.kind == Kind::Table)
3947 .expect("a warning about the table");
3948 assert_eq!(table_warning.fidelity, Fidelity::Degraded);
3949 }
3950
3951 #[test]
3952 fn container_origin_separates_a_div_from_a_div() {
3953 let mut html =
3958 Editor::new("<div>hi</div>\n".as_bytes(), Format::Html).expect("html editor");
3959 let mut md = Editor::new_ext(
3960 ":::div\nhi\n:::\n".as_bytes(),
3961 Format::Markdown,
3962 MarkdownExtensions {
3963 directives: true,
3964 ..Default::default()
3965 },
3966 )
3967 .expect("markdown editor");
3968
3969 let html_nodes = html.nodes().expect("html nodes");
3970 let md_nodes = md.nodes().expect("markdown nodes");
3971 let tag = html_nodes
3972 .iter()
3973 .find(|n| n.name.as_deref() == Some("div"))
3974 .expect("a <div> container");
3975 let directive = md_nodes
3976 .iter()
3977 .find(|n| n.name.as_deref() == Some("div"))
3978 .expect("a :::div container");
3979
3980 assert_eq!(tag.kind, directive.kind);
3982 assert_eq!(tag.name, directive.name);
3983 assert_eq!(tag.directive_form, directive.directive_form);
3984 assert_eq!(tag.directive_form, Some(DirectiveForm::Container));
3985
3986 assert_eq!(tag.origin, Some(ContainerOrigin::Element));
3988 assert_eq!(directive.origin, Some(ContainerOrigin::Directive));
3989 }
3990
3991 fn for_both_formats(src: &str, check: impl Fn(&mut Document, Format)) {
3995 for format in [Format::Markdown, Format::Djot] {
3996 let mut doc = Document::parse(src.as_bytes(), format).expect("parse");
3997 check(&mut doc, format);
3998 }
3999 }
4000
4001 #[test]
4002 fn marker_span_is_what_a_rich_view_hides() {
4003 for_both_formats("> - [x] done\n", |doc, format| {
4004 let nodes = doc.nodes().expect("nodes");
4005 let quote = nodes
4006 .iter()
4007 .find(|n| n.kind == Kind::BlockQuote)
4008 .expect("a block quote");
4009 let item = nodes
4010 .iter()
4011 .find(|n| n.kind == Kind::TaskListItem)
4012 .expect("a task item");
4013
4014 assert_eq!(quote.marker_span, Some(0..2), "{format:?}");
4018 assert_eq!(item.marker_span, Some(2..8), "{format:?}");
4019
4020 assert_eq!(quote.content_span, Some(quote.span.clone()), "{format:?}");
4024
4025 let para = nodes
4027 .iter()
4028 .find(|n| n.kind == Kind::Para)
4029 .expect("a paragraph");
4030 assert_eq!(para.marker_span, None, "{format:?}");
4031 });
4032 }
4033
4034 #[test]
4035 fn attrs_span_locates_the_attribute_block_a_heuristic_had_to_guess_at() {
4036 let src = "{.vis .family}\nheld back\n\nplain\n";
4042 let mut doc = Document::parse(src.as_bytes(), Format::Djot).expect("parse");
4043 let nodes = doc.nodes().expect("nodes");
4044 let paras: Vec<&FlatNode> = nodes.iter().filter(|n| n.kind == Kind::Para).collect();
4045 assert_eq!(paras.len(), 2);
4046
4047 let span = doc
4048 .attrs_span(paras[0].id)
4049 .expect("attrs span")
4050 .expect("the attributed paragraph has one");
4051 assert_eq!(&src[span.clone()], "{.vis .family}");
4052 assert!(span.end <= paras[0].span.start);
4055
4056 assert_eq!(doc.attrs_span(paras[1].id).expect("attrs span"), None);
4059 }
4060
4061 #[test]
4062 fn line_prefix_assembles_every_marker_on_the_line() {
4063 for_both_formats("> - [x] done\n", |doc, format| {
4064 assert_eq!(doc.line_prefix(9).expect("prefix"), Some(0..8), "{format:?}");
4067 });
4068 }
4069
4070 #[test]
4071 fn line_prefix_is_none_on_a_continuation_line() {
4072 for_both_formats("> c\n> d\n", |doc, format| {
4078 assert_eq!(doc.line_prefix(2).expect("prefix"), Some(0..2), "{format:?}");
4079 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
4080 });
4081 }
4082
4083 #[test]
4084 fn a_caret_at_a_blocks_end_is_in_that_block_in_both_formats() {
4085 for_both_formats("a\n\nb\n", |doc, format| {
4091 for offset in [0usize, 1, 3, 4] {
4092 let hit = doc
4093 .node_at_caret(offset)
4094 .expect("caret hit")
4095 .expect("some node");
4096 assert_eq!(hit.kind, Kind::Str, "{format:?} at {offset}");
4097 }
4098 for offset in [2usize, 5] {
4101 let hit = doc
4102 .node_at_caret(offset)
4103 .expect("caret hit")
4104 .expect("some node");
4105 assert_eq!(hit.kind, Kind::Doc, "{format:?} at {offset}");
4106 }
4107 });
4108 }
4109
4110 #[test]
4111 fn the_caret_chain_ends_at_the_node_the_scalar_call_returns() {
4112 for_both_formats("- a\n", |doc, format| {
4113 let hit = doc.node_at_caret(3).expect("hit").expect("some node");
4114 let chain = doc.ancestors_at_caret(3).expect("chain");
4115 assert_eq!(chain.last().map(|m| m.node_id), Some(hit.node_id), "{format:?}");
4116 assert!(
4119 chain.iter().any(|m| m.kind == Kind::ListItem),
4120 "{format:?}: chain should reach the list item"
4121 );
4122 });
4123 }
4124
4125 #[test]
4126 fn continuation_prefix_repeats_a_quote_and_indents_past_an_item() {
4127 for_both_formats("> - a\n", |doc, format| {
4128 assert_eq!(doc.line_prefix(4).expect("prefix"), Some(0..4), "{format:?}");
4133 let cont = doc.continuation_prefix(4).expect("continuation");
4134 assert_eq!(cont.text, "> ", "{format:?}");
4135 assert_eq!(cont.columns, 4, "{format:?}");
4136 });
4137 }
4138
4139 #[test]
4140 fn continuation_prefix_answers_on_a_line_that_opens_nothing() {
4141 for_both_formats("> c\n> d\n", |doc, format| {
4144 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
4145 assert_eq!(
4146 doc.continuation_prefix(6).expect("continuation").text,
4147 "> ",
4148 "{format:?}"
4149 );
4150 });
4151 }
4152
4153 #[test]
4154 fn continuation_prefix_takes_an_ordered_markers_own_width() {
4155 for_both_formats("10. x\n", |doc, format| {
4158 assert_eq!(
4159 doc.continuation_prefix(4).expect("continuation").columns,
4160 4,
4161 "{format:?}"
4162 );
4163 });
4164 for_both_formats("1. x\n", |doc, format| {
4165 assert_eq!(
4166 doc.continuation_prefix(3).expect("continuation").columns,
4167 3,
4168 "{format:?}"
4169 );
4170 });
4171 }
4172
4173 #[test]
4174 fn a_blank_line_keeps_a_quote_alive_and_drops_an_items_indent() {
4175 for_both_formats("> - a\n", |doc, format| {
4176 let blank = doc.blank_line_prefix(4).expect("blank");
4177 assert_eq!(blank.text, ">", "{format:?}");
4180 assert_eq!(blank.columns, 1, "{format:?}");
4181 });
4182 for_both_formats("- a\n", |doc, format| {
4185 assert_eq!(doc.blank_line_prefix(3).expect("blank").text, "", "{format:?}");
4186 });
4187 }
4188
4189 #[test]
4190 fn a_prefix_column_count_is_not_its_byte_length() {
4191 let mut doc = Document::parse("- x
4194".as_bytes(), Format::Markdown).expect("parse");
4195 let cont = doc.continuation_prefix(2).expect("continuation");
4196 assert_eq!(cont.columns, 4);
4197 }
4198
4199 #[test]
4200 fn set_block_opens_a_heading_on_a_blank_line() {
4201 for format in [Format::Markdown, Format::Djot] {
4202 let mut ed = Editor::new("a\n\n".as_bytes(), format).expect("editor");
4203 ed.set_block(3, BlockKind::Heading(2)).expect("set_block");
4204 assert_eq!(ed.source().expect("source"), b"a\n\n## ", "{format:?}");
4205 let nodes = ed.nodes().expect("nodes");
4209 assert!(
4210 nodes.iter().any(|n| n.kind == Kind::Heading),
4211 "{format:?}: should have parsed a heading"
4212 );
4213 }
4214 }
4215
4216 #[test]
4217 fn set_block_refuses_a_blank_line_inside_a_code_block() {
4218 for format in [Format::Markdown, Format::Djot] {
4222 let src = "```\nx\n\ny\n```\n";
4223 let mut ed = Editor::new(src.as_bytes(), format).expect("editor");
4224 let blank = src.find("\n\n").expect("a blank line") + 1;
4225 assert!(
4226 matches!(
4227 ed.set_block(blank, BlockKind::Heading(1)),
4228 Err(Error::NotEditable)
4229 ),
4230 "{format:?}"
4231 );
4232 assert_eq!(ed.source().expect("source"), src.as_bytes(), "{format:?}");
4233 }
4234 }
4235
4236 #[test]
4237 fn task_items_report_their_checkbox_state() {
4238 for_both_formats("- [ ] a\n- [x] b\n- [X] c\n- d\n", |doc, format| {
4242 let nodes = doc.nodes().expect("nodes");
4243 let states: Vec<Option<bool>> = nodes
4244 .iter()
4245 .filter(|n| matches!(n.kind, Kind::TaskListItem | Kind::ListItem))
4246 .map(|n| n.checked)
4247 .collect();
4248 assert_eq!(
4249 states,
4250 vec![Some(false), Some(true), Some(true), None],
4251 "{format:?}"
4252 );
4253
4254 for n in nodes.iter().filter(|n| n.kind == Kind::Para) {
4257 assert_eq!(n.checked, None, "{format:?}");
4258 }
4259 });
4260 }
4261
4262 #[test]
4263 fn an_editor_reaches_the_caret_reads_through_its_document_view() {
4264 let mut ed = Editor::new("- a\n".as_bytes(), Format::Markdown).expect("editor");
4269 let mut view = ed.document().expect("document view");
4270
4271 assert_eq!(view.line_prefix(3).expect("prefix"), Some(0..2));
4272 let hit = view.node_at_caret(3).expect("hit").expect("some node");
4273 assert_eq!(hit.kind, Kind::Str);
4274 }
4275
4276 #[test]
4277 fn container_origin_is_none_for_non_containers() {
4278 let mut ed = Editor::new("# hi\n\npara\n".as_bytes(), Format::Markdown).expect("editor");
4281 for n in ed.nodes().expect("nodes") {
4282 assert_eq!(n.origin, None, "{} should carry no origin", n.kind);
4283 }
4284 }
4285
4286 #[test]
4287 fn flat_nodes_expose_directive_name_and_form() {
4288 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
4294 let mut ed = Editor::new_ext(
4295 src.as_bytes(),
4296 Format::Markdown,
4297 MarkdownExtensions {
4298 directives: true,
4299 ..Default::default()
4300 },
4301 )
4302 .expect("editor");
4303 let nodes = ed.nodes().expect("nodes");
4304
4305 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
4306 .iter()
4307 .filter(|n| n.kind == Kind::Container)
4308 .map(|n| (n.name.as_deref(), n.directive_form))
4309 .collect();
4310 assert_eq!(
4311 forms,
4312 vec![
4313 (Some("note"), Some(DirectiveForm::Container)),
4314 (Some("embed"), Some(DirectiveForm::Leaf)),
4315 (Some("abbr"), Some(DirectiveForm::Text)),
4316 ]
4317 );
4318
4319 let embed = nodes
4322 .iter()
4323 .find(|n| n.name.as_deref() == Some("embed"))
4324 .expect("embed");
4325 assert_eq!(
4326 embed.attrs,
4327 vec![("src".to_string(), Some("demo.html".to_string()))]
4328 );
4329 let para = nodes.iter().find(|n| n.kind == Kind::Para).expect("a para");
4330 assert!(para.directive_form.is_none() && para.name.is_none());
4331 }
4332
4333 #[test]
4334 fn editor_insert_child_and_delete() {
4335 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
4336 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4337 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
4338 ed.delete("0.1").expect("delete");
4339 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
4340 }
4341
4342 #[test]
4343 fn editor_edits_by_selector() {
4344 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4345 ed.replace("heading(\"Two\")", "## Renamed")
4346 .expect("replace");
4347 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
4348 }
4349
4350 #[test]
4351 fn editor_locator_errors_are_distinct() {
4352 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
4353 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
4354 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
4355 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
4356 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
4358 }
4359
4360 #[test]
4361 fn editor_reparse_break_rolls_back() {
4362 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
4363 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
4364 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
4365 }
4366
4367 #[test]
4368 fn editor_leaf_content_is_not_editable() {
4369 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4370 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
4371 }
4372
4373 #[test]
4374 fn editor_query_reflects_current_tree() {
4375 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
4376 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4377 assert_eq!(ed.query("element").expect("query").len(), 3);
4379 let json = ed.ast_json().expect("ast_json");
4380 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
4381 }
4382
4383 #[test]
4386 fn editor_edit_range_types_backspaces_and_reports_change() {
4387 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4388
4389 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
4391 assert_eq!(ed.source_str().unwrap(), "aXb\n");
4392 assert_eq!(c.old, 1..1);
4393 assert_eq!(c.new, 1..2);
4394 assert_eq!(c.delta(), 1);
4395
4396 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
4398 assert_eq!(ed.source_str().unwrap(), "ab\n");
4399 assert_eq!(c2.old, 1..2);
4400 assert_eq!(c2.new, 1..1);
4401 assert_eq!(c2.delta(), -1);
4402 }
4403
4404 #[test]
4405 fn editor_edit_range_rejects_bad_ranges() {
4406 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4407 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"); }
4411
4412 #[test]
4413 fn editor_last_change_reports_locator_ops_too() {
4414 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4415 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed")
4418 .expect("replace");
4419 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
4420 let c = ed.last_change().expect("a change was recorded");
4421 assert_eq!(c.old, 7..13);
4423 assert_eq!(c.new, 7..17);
4424 }
4425
4426 #[test]
4427 fn editor_nodes_is_a_walkable_flat_tree() {
4428 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4429 let nodes = ed.nodes().expect("nodes");
4430 assert!(!nodes.is_empty());
4431
4432 for (i, n) in nodes.iter().enumerate() {
4434 assert_eq!(n.id, NodeId(i as u32));
4435 }
4436 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
4438 assert_eq!(roots.len(), 1);
4439 assert_eq!(roots[0].kind, Kind::Doc);
4440
4441 let heading = nodes
4443 .iter()
4444 .find(|n| n.kind == Kind::Heading)
4445 .expect("a heading");
4446 assert_eq!(heading.level, Some(1));
4447 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
4448
4449 assert_eq!(heading.head, None);
4451 assert_eq!(heading.alignment, None);
4452
4453 for n in nodes.iter().filter(|n| n.parent.is_some()) {
4456 let p = &nodes[n.parent.unwrap().0 as usize];
4457 let mut kid = p.first_child;
4458 let mut seen = false;
4459 while let Some(NodeId(k)) = kid {
4460 if k == n.id.0 {
4461 seen = true;
4462 break;
4463 }
4464 kid = nodes[k as usize].next_sibling;
4465 }
4466 assert!(
4467 seen,
4468 "node {:?} not found among its parent's children",
4469 n.id
4470 );
4471 }
4472 }
4473
4474 #[test]
4475 fn editor_child_spans_and_subtree_agree_with_nodes() {
4476 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
4477 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4478 let all = ed.nodes().expect("nodes");
4479 let doc = all.iter().find(|n| n.kind == Kind::Doc).expect("doc");
4480
4481 let top = ed.child_spans(None).expect("child_spans");
4484 let mut want = Vec::new();
4485 let mut c = doc.first_child;
4486 while let Some(id) = c {
4487 want.push(id);
4488 c = all[id.0 as usize].next_sibling;
4489 }
4490 assert_eq!(top.len(), want.len(), "top-level count");
4491 for (m, id) in top.iter().zip(&want) {
4492 assert_eq!(m.node_id, id.0, "child id");
4493 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
4494 assert_eq!(m.span, all[id.0 as usize].span, "child span");
4495 }
4496 assert!(
4498 src[top[0].span.clone()].starts_with('#'),
4499 "first block is the heading"
4500 );
4501
4502 let list = top
4504 .iter()
4505 .find(|m| {
4506 matches!(
4507 m.kind,
4508 Kind::BulletList | Kind::OrderedList | Kind::TaskList
4509 )
4510 })
4511 .expect("a list");
4512 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
4513 assert_eq!(items.len(), 2);
4514 assert!(
4515 items.iter().all(|m| m.kind == Kind::ListItem),
4516 "items: {items:?}"
4517 );
4518
4519 let para = top
4521 .iter()
4522 .find(|m| m.kind == Kind::Para)
4523 .expect("a para")
4524 .node_id;
4525 let sub = ed.subtree(NodeId(para)).expect("subtree");
4526 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
4527 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
4528 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
4529 assert_eq!(sub[0].kind, Kind::Para);
4530 for (i, n) in sub.iter().enumerate() {
4531 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
4532 for link in [n.parent, n.first_child, n.next_sibling]
4533 .into_iter()
4534 .flatten()
4535 {
4536 assert!(
4537 (link.0 as usize) < sub.len(),
4538 "link {link:?} escapes the subtree"
4539 );
4540 }
4541 }
4542 assert!(
4543 src[sub[0].span.clone()].starts_with("Hello"),
4544 "absolute span: {:?}",
4545 &src[sub[0].span.clone()]
4546 );
4547
4548 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<Kind> {
4550 let mut out = Vec::new();
4551 let mut stack = vec![root];
4552 while let Some(id) = stack.pop() {
4553 let n = &all[id.0 as usize];
4554 out.push(n.kind.clone());
4555 let mut c = n.first_child;
4556 while let Some(cid) = c {
4557 stack.push(cid);
4558 c = all[cid.0 as usize].next_sibling;
4559 }
4560 }
4561 out
4562 }
4563 let mut want_kinds = arena_kinds(&all, NodeId(para));
4564 let mut got_kinds: Vec<Kind> = sub.iter().map(|n| n.kind.clone()).collect();
4565 want_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4569 got_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4570 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
4571
4572 assert!(matches!(
4574 ed.subtree(NodeId(9999)),
4575 Err(Error::InvalidArgument)
4576 ));
4577 }
4578
4579 #[test]
4580 fn flat_nodes_carry_table_head_and_alignment() {
4581 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
4585 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4586 let nodes = ed.nodes().expect("nodes");
4587
4588 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Row).collect();
4589 assert_eq!(rows.len(), 2, "a header row and one body row");
4590 assert_eq!(rows[0].head, Some(true), "first row is the header");
4591 assert_eq!(rows[1].head, Some(false), "second row is a body row");
4592
4593 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Cell).collect();
4594 assert_eq!(cells.len(), 4);
4595 assert_eq!(cells[0].alignment, Some(Alignment::Left));
4597 assert_eq!(cells[1].alignment, Some(Alignment::Right));
4598 assert_eq!(cells[2].alignment, Some(Alignment::Left));
4599 assert_eq!(cells[3].alignment, Some(Alignment::Right));
4600 assert_eq!(cells[0].head, Some(true));
4602 assert_eq!(cells[2].head, Some(false));
4603
4604 let mut plain =
4607 Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
4608 let pnodes = plain.nodes().expect("nodes");
4609 let pcell = pnodes
4610 .iter()
4611 .find(|n| n.kind == Kind::Cell)
4612 .expect("a cell");
4613 assert_eq!(pcell.alignment, Some(Alignment::Default));
4614 }
4615
4616 #[test]
4617 fn cell_extent_reports_merged_cells_and_nothing_else() {
4618 let src = "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td><td>b</td></tr></table>";
4619 let mut doc = Document::parse_str(src, Format::Html).expect("parse");
4620 let cells: Vec<NodeId> = doc
4621 .nodes()
4622 .expect("nodes")
4623 .iter()
4624 .filter(|n| n.kind == Kind::Cell)
4625 .map(|n| n.id)
4626 .collect();
4627 assert_eq!(cells.len(), 2);
4628 assert_eq!(doc.cell_extent(cells[0]).expect("extent"), Some((2, 3)));
4629 assert_eq!(doc.cell_extent(cells[1]).expect("extent"), Some((1, 1)));
4631
4632 let mut pipe =
4634 Document::parse_str("| a |\n| --- |\n| b |\n", Format::Markdown).expect("parse");
4635 let pipe_cell = pipe
4636 .nodes()
4637 .expect("nodes")
4638 .iter()
4639 .find(|n| n.kind == Kind::Cell)
4640 .expect("a cell")
4641 .id;
4642 assert_eq!(pipe.cell_extent(pipe_cell).expect("extent"), Some((1, 1)));
4643
4644 let root = NodeId(0);
4646 assert_eq!(pipe.cell_extent(root).expect("extent"), None);
4647 }
4648
4649 #[test]
4650 fn builder_add_cell_spanning_renders_colspan_and_rowspan() {
4651 let mut b = Builder::new().expect("builder");
4652 let wide_text = b.add_text(TextKind::Str, "wide").expect("str");
4653 let wide = b
4654 .add_cell_spanning(false, Alignment::Default, 2, 3)
4655 .expect("cell");
4656 b.set_children(wide, &[wide_text]).expect("children");
4657 let plain_text = b.add_text(TextKind::Str, "one").expect("str");
4658 let plain = b.add_cell(false, Alignment::Default).expect("cell");
4659 b.set_children(plain, &[plain_text]).expect("children");
4660 let row = b.add_row(false).expect("row");
4661 b.set_children(row, &[wide, plain]).expect("children");
4662 let table = b.add(VoidKind::Table).expect("table");
4663 b.set_children(table, &[row]).expect("children");
4664
4665 let html = String::from_utf8(b.render_html(table).expect("html")).expect("utf-8");
4666 assert!(
4667 html.contains("<td colspan=\"2\" rowspan=\"3\">wide</td>"),
4668 "{html}"
4669 );
4670 assert!(html.contains("<td>one</td>"), "{html}");
4672
4673 assert!(matches!(
4675 b.add_cell_spanning(false, Alignment::Default, 0, 1),
4676 Err(Error::InvalidArgument)
4677 ));
4678 }
4679
4680 #[test]
4681 fn editor_node_at_and_ancestors_hit_test_offsets() {
4682 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4683
4684 let m = ed
4686 .node_at(2)
4687 .expect("node_at")
4688 .expect("a node covers offset 2");
4689 assert!(m.span.contains(&2));
4690
4691 let chain = ed.ancestors_at(2).expect("ancestors_at");
4693 assert!(!chain.is_empty());
4694 assert_eq!(chain[0].kind, Kind::Doc);
4695 assert_eq!(chain.last().unwrap().node_id, m.node_id);
4696
4697 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
4699 }
4700
4701 #[test]
4704 fn editor_wrap_and_toggle_inline_round_trip() {
4705 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4706
4707 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
4709 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
4710 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
4711
4712 ed.toggle_inline(4, 8, InlineKind::Strong)
4714 .expect("toggle off");
4715 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4716
4717 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
4719 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
4720 }
4721
4722 #[test]
4723 fn editor_inline_marks_cut_at_block_boundaries() {
4724 let mut ed = Editor::new_str("one two\n\nthree four\n", Format::Markdown)
4727 .expect("editor");
4728 let c = ed.toggle_inline(0, 19, InlineKind::Strong).expect("toggle on");
4729 assert_eq!(
4730 ed.source_str().unwrap(),
4731 "**one two**\n\n**three four**\n"
4732 );
4733
4734 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**one two**\n\n**three four**");
4737 ed.undo().expect("undo");
4738 assert_eq!(ed.source_str().unwrap(), "one two\n\nthree four\n");
4739
4740 ed.toggle_inline(0, 19, InlineKind::Strong).expect("toggle on");
4743 ed.toggle_inline(0, 27, InlineKind::Strong).expect("toggle off");
4744 assert_eq!(ed.source_str().unwrap(), "one two\n\nthree four\n");
4745
4746 let mut fenced = Editor::new_str("```\nx y\n```\n", Format::Markdown).expect("editor");
4748 assert_eq!(
4749 fenced.toggle_inline(4, 7, InlineKind::Strong),
4750 Err(Error::NotEditable)
4751 );
4752 }
4753
4754 #[test]
4755 fn editor_inline_kind_support_is_format_specific() {
4756 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4758 assert_eq!(
4759 md.wrap_range(2, 6, InlineKind::Mark),
4760 Err(Error::UnsupportedFormat)
4761 );
4762
4763 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4765 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
4766 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
4767 }
4768
4769 #[test]
4770 fn editor_authors_gfm_strikethrough_out_of_the_box() {
4771 assert!(Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Delete)));
4775 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4776 ed.toggle_inline(2, 6, InlineKind::Delete).expect("strike");
4777 assert_eq!(ed.source_str().unwrap(), "a ~~word~~ b\n");
4778 ed.toggle_inline(4, 8, InlineKind::Delete).expect("unstrike");
4779 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4780 }
4781
4782 #[test]
4783 fn editor_highlight_is_authorable_with_the_extension_on() {
4784 let exts = MarkdownExtensions {
4785 highlight: true,
4786 ..Default::default()
4787 };
4788 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
4792 assert!(Format::Markdown.supports_with(exts, Gesture::ToggleInline(InlineKind::Mark)));
4793
4794 let mut ed =
4795 Editor::new_ext(b"a word b\n", Format::Markdown, exts).expect("editor");
4796 ed.toggle_inline(2, 6, InlineKind::Mark).expect("highlight");
4797 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4798 ed.toggle_inline(4, 8, InlineKind::Mark).expect("unhighlight");
4799 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4800 }
4801
4802 #[test]
4803 fn editor_set_mark_color_writes_reads_and_clears_the_colour() {
4804 let exts = MarkdownExtensions {
4805 highlight: true,
4806 highlight_colors: true,
4807 ..Default::default()
4808 };
4809 assert!(Format::Markdown.supports_with(exts, Gesture::SetMarkColor));
4810 let hi_only = MarkdownExtensions {
4812 highlight: true,
4813 ..Default::default()
4814 };
4815 assert!(!Format::Markdown.supports_with(hi_only, Gesture::SetMarkColor));
4816 assert!(!Format::Markdown.supports(Gesture::SetMarkColor));
4817 assert!(!Format::Djot.supports_with(exts, Gesture::SetMarkColor));
4818
4819 let mut ed =
4820 Editor::new_ext("a ==word== b\n".as_bytes(), Format::Markdown, exts).expect("editor");
4821 ed.set_mark_color(6, Some(MarkColor::Red)).expect("colour");
4822 assert_eq!(ed.source_str().unwrap(), "a ==\u{1F534} word== b\n");
4823
4824 let mut doc =
4826 Document::parse_with(ed.source_str().unwrap().as_bytes(), Format::Markdown, exts)
4827 .expect("parse");
4828 assert_eq!(doc.query("mark[data-color=red]").expect("query").len(), 1);
4829
4830 ed.set_mark_color(9, Some(MarkColor::Blue)).expect("recolour");
4831 assert_eq!(ed.source_str().unwrap(), "a ==\u{1F535} word== b\n");
4832 ed.set_mark_color(9, None).expect("clear");
4833 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4834
4835 assert_eq!(
4837 ed.set_mark_color(0, Some(MarkColor::Red)),
4838 Err(Error::NotEditable)
4839 );
4840 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4841
4842 for c in [
4844 MarkColor::Red,
4845 MarkColor::Orange,
4846 MarkColor::Yellow,
4847 MarkColor::Green,
4848 MarkColor::Blue,
4849 MarkColor::Purple,
4850 MarkColor::Brown,
4851 ] {
4852 assert_eq!(MarkColor::from_str(c.as_str()), Some(c));
4853 }
4854 assert_eq!(MarkColor::from_str("pink"), None);
4855 }
4856
4857 #[test]
4858 fn editor_toggle_strips_verbatim_via_content_span() {
4859 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
4860 ed.toggle_inline(2, 8, InlineKind::Verbatim)
4862 .expect("toggle code off");
4863 assert_eq!(ed.source_str().unwrap(), "a code b\n");
4864
4865 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
4868 ed2.toggle_inline(2, 7, InlineKind::Verbatim)
4869 .expect("toggle multi off");
4870 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
4871 }
4872
4873 #[test]
4874 fn editor_set_block_switches_para_and_heading_levels() {
4875 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
4876
4877 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
4879 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
4880
4881 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
4883 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
4884
4885 ed.set_block(2, BlockKind::Paragraph).expect("to para");
4887 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
4888 }
4889
4890 #[test]
4891 fn editor_set_block_rejects_bad_level_and_format() {
4892 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4893 assert_eq!(
4894 md.set_block(0, BlockKind::Heading(9)),
4895 Err(Error::InvalidArgument)
4896 );
4897
4898 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4899 assert_eq!(
4900 xml.set_block(1, BlockKind::Heading(1)),
4901 Err(Error::UnsupportedFormat)
4902 );
4903 }
4904
4905 #[test]
4906 fn editor_toggle_block_container_round_trips() {
4907 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
4908
4909 let c = ed
4910 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
4911 .expect("quote on");
4912 assert_eq!(ed.source_str().unwrap(), "> a\n");
4913 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
4914
4915 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4916 .expect("quote off");
4917 assert_eq!(ed.source_str().unwrap(), "a\n");
4918 }
4919
4920 #[test]
4921 fn editor_toggle_block_container_nests_a_partial_selection() {
4922 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
4923
4924 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4927 .expect("nest");
4928 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
4929
4930 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
4932 .expect("peel");
4933 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
4934 }
4935
4936 #[test]
4937 fn editor_toggle_block_container_numbers_and_converts_lists() {
4938 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
4939
4940 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
4942 .expect("ordered on");
4943 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
4944
4945 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
4947 .expect("convert");
4948 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
4949 }
4950
4951 #[test]
4952 fn editor_toggle_block_container_rejects_unspellable_format() {
4953 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4954 assert_eq!(
4955 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
4956 Err(Error::UnsupportedFormat)
4957 );
4958 }
4959
4960 #[test]
4961 fn editor_insert_link_wraps_and_repoints() {
4962 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4963
4964 ed.insert_link(2, 6, "http://x.dev").expect("link");
4965 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
4966
4967 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
4969 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
4970 }
4971
4972 #[test]
4973 fn editor_insert_link_repoints_an_autolink() {
4974 for format in [Format::Markdown, Format::Djot] {
4979 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
4980 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
4981 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
4982
4983 let nodes = ed.nodes().expect("nodes");
4985 let url = nodes
4986 .iter()
4987 .find(|n| n.kind == Kind::Url)
4988 .expect("still an autolink");
4989 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
4990 assert!(!nodes.iter().any(|n| n.kind == Kind::Link));
4991 }
4992 }
4993
4994 #[test]
4995 fn editor_insert_link_escapes_the_destination() {
4996 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4999 dj.insert_link(0, 1, "a)b").expect("link");
5000 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
5001
5002 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
5006 md.insert_link(0, 1, "a b").expect("link");
5007 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
5008
5009 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
5010 dj2.insert_link(0, 1, "a b").expect("link");
5011 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
5012 }
5013
5014 #[test]
5015 fn editor_insert_image_escapes_the_destination_per_format() {
5016 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
5019 md.insert_image(0, 1, "my cat.png").expect("image");
5020 assert_eq!(md.source_str().unwrap(), "\n");
5021
5022 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
5023 dj.insert_image(0, 1, "my cat.png").expect("image");
5024 assert_eq!(dj.source_str().unwrap(), "\n");
5025
5026 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
5028 paren.insert_image(0, 1, "a)b.png").expect("image");
5029 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
5030 }
5031
5032 #[test]
5033 fn editor_insert_image_keeps_an_empty_alt_empty() {
5034 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5037 ed.insert_image(1, 1, "cat.png").expect("image");
5038 assert_eq!(ed.source_str().unwrap(), "ab\n");
5039 }
5040
5041 #[test]
5042 fn editor_insert_image_rejects_a_newline_destination() {
5043 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
5044 assert_eq!(
5045 ed.insert_image(0, 1, "a\nb.png"),
5046 Err(Error::InvalidArgument)
5047 );
5048
5049 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5050 assert_eq!(
5051 xml.insert_image(3, 5, "x.png"),
5052 Err(Error::UnsupportedFormat)
5053 );
5054 }
5055
5056 #[test]
5057 fn editor_insert_link_rejects_a_newline_destination() {
5058 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
5059 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
5060
5061 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5062 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
5063 }
5064
5065 #[test]
5066 fn editor_insert_literal_keeps_typed_specials_literal() {
5067 for format in [Format::Markdown, Format::Djot] {
5068 let mut ed = Editor::new_str("z\n", format).expect("editor");
5069 ed.insert_literal(0, "*hi*").expect("literal");
5071
5072 let nodes = ed.nodes().expect("nodes");
5074 assert!(
5075 !nodes
5076 .iter()
5077 .any(|n| n.kind == Kind::Emph || n.kind == Kind::Strong)
5078 );
5079 let text: String = nodes
5080 .iter()
5081 .filter(|n| n.kind == Kind::Str)
5082 .filter_map(|n| n.text.clone())
5083 .collect();
5084 assert_eq!(text, "*hi*z");
5085 }
5086 }
5087
5088 #[test]
5089 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
5090 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
5092 ed.insert_literal(1, "# ").expect("literal");
5093 assert_eq!(ed.source_str().unwrap(), "a# z\n");
5094
5095 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
5097 ed2.insert_literal(0, "# ").expect("literal");
5098 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
5099 assert!(
5100 !ed2.nodes()
5101 .expect("nodes")
5102 .iter()
5103 .any(|n| n.kind == Kind::Heading)
5104 );
5105 }
5106
5107 #[test]
5108 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
5109 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5110 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
5111
5112 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5113 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
5114 }
5115
5116 #[test]
5117 fn editor_insert_line_break_splices_in_cell_br() {
5118 let mut ed =
5119 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
5120 ed.insert_line_break(3).expect("line break");
5122 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
5123 let nodes = ed.nodes().expect("nodes");
5125 assert!(nodes.iter().any(|n| n.kind == Kind::HardBreak));
5126 assert!(!nodes.iter().any(|n| n.kind == Kind::RawInline));
5127 }
5128
5129 #[test]
5130 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
5131 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
5133 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
5134
5135 let mut dj = Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
5137 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
5138
5139 let mut ed =
5141 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
5142 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
5143 }
5144
5145 #[test]
5146 fn editor_insert_thematic_break_is_blank_separated_per_format() {
5147 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
5151 md.insert_thematic_break(0).expect("rule");
5152 assert_eq!(md.source_str().unwrap(), "a\n\n---\n");
5153 let nodes = md.nodes().expect("nodes");
5154 assert!(nodes.iter().any(|n| n.kind == Kind::ThematicBreak));
5155 assert!(!nodes.iter().any(|n| n.kind == Kind::Heading));
5156
5157 let mut dj = Editor::new_str("a\n", Format::Djot).expect("editor");
5160 dj.insert_thematic_break(0).expect("rule");
5161 assert_eq!(dj.source_str().unwrap(), "a\n\n* * *\n");
5162
5163 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5164 assert_eq!(xml.insert_thematic_break(3), Err(Error::UnsupportedFormat));
5165 }
5166
5167 #[test]
5168 fn editor_split_block_keeps_both_halves_the_same_kind() {
5169 let mut item = Editor::new_str("- this is a list item\n", Format::Markdown).expect("editor");
5172 item.split_block(10).expect("split");
5173 assert_eq!(item.source_str().unwrap(), "- this is \n- a list item\n");
5174 let nodes = item.nodes().expect("nodes");
5175 assert_eq!(nodes.iter().filter(|n| n.kind == Kind::ListItem).count(), 2);
5176
5177 let mut tail = Editor::new_str("- a\n", Format::Markdown).expect("editor");
5179 tail.split_block(3).expect("split");
5180 assert_eq!(tail.source_str().unwrap(), "- a\n- \n");
5181
5182 let mut para = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5184 para.split_block(1).expect("split");
5185 assert_eq!(para.source_str().unwrap(), "a\n\nb\n");
5186
5187 let mut table =
5189 Editor::new_str("| a | b |\n|---|---|\n| c | d |\n", Format::Markdown).expect("editor");
5190 assert_eq!(table.split_block(3), Err(Error::NotEditable));
5191
5192 let mut empty = Editor::new_str("", Format::Markdown).expect("editor");
5193 assert_eq!(empty.split_block(0), Err(Error::NotFound));
5194 }
5195
5196 #[test]
5197 fn editor_toggle_code_block_round_trips_and_measures_the_fence() {
5198 let mut ed = Editor::new_str("a\n", Format::Markdown).expect("editor");
5199 ed.toggle_code_block(0, 1, Some("zig")).expect("fence");
5200 assert_eq!(ed.source_str().unwrap(), "```zig\na\n```\n");
5201 let nodes = ed.nodes().expect("nodes");
5202 assert!(nodes.iter().any(|n| n.kind == Kind::CodeBlock));
5203
5204 ed.toggle_code_block(0, 0, None).expect("unfence");
5205 assert_eq!(ed.source_str().unwrap(), "a\n");
5206
5207 let mut runs = Editor::new_str("a ``` b\n", Format::Markdown).expect("editor");
5210 runs.toggle_code_block(0, 7, None).expect("fence");
5211 assert_eq!(runs.source_str().unwrap(), "````\na ``` b\n````\n");
5212 }
5213
5214 #[test]
5215 fn editor_toggle_code_block_refuses_inside_a_list_item() {
5216 let mut ed = Editor::new_str("- a\n- b\n", Format::Markdown).expect("editor");
5219 assert_eq!(ed.toggle_code_block(2, 3, None), Err(Error::NotEditable));
5220 assert_eq!(ed.source_str().unwrap(), "- a\n- b\n");
5221 }
5222
5223 #[test]
5224 fn editor_set_code_language_retags_clears_and_refuses() {
5225 let mut ed = Editor::new_str("```zig\na\n```\n", Format::Markdown).expect("editor");
5226 ed.set_code_language(0, Some("rust")).expect("retag");
5227 assert_eq!(ed.source_str().unwrap(), "```rust\na\n```\n");
5228
5229 ed.set_code_language(0, None).expect("clear");
5232 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
5233 ed.set_code_language(0, Some("")).expect("empty");
5234 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
5235
5236 assert_eq!(
5239 ed.set_code_language(0, Some("a b")),
5240 Err(Error::InvalidArgument)
5241 );
5242 let mut dj = Editor::new_str("```\na\n```\n", Format::Djot).expect("editor");
5244 dj.set_code_language(0, Some("a b"))
5245 .expect("djot info string");
5246 assert_eq!(dj.source_str().unwrap(), "```a b\na\n```\n");
5247
5248 let mut para = Editor::new_str("x\n", Format::Markdown).expect("editor");
5249 assert_eq!(para.set_code_language(0, Some("zig")), Err(Error::NotFound));
5250 }
5251
5252 #[test]
5253 fn editor_task_checkbox_gestures() {
5254 let mut ed = Editor::new_str("- a\n", Format::Markdown).expect("editor");
5255
5256 ed.toggle_task_item(2).expect("add box");
5259 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
5260 assert!(
5261 ed.nodes()
5262 .unwrap()
5263 .iter()
5264 .any(|n| n.kind == Kind::TaskListItem)
5265 );
5266
5267 ed.set_task_checked(6, true).expect("tick");
5268 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
5269 ed.set_task_checked(6, true).expect("no-op");
5271 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
5272
5273 ed.toggle_task_checked(6).expect("flip");
5274 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
5275
5276 ed.toggle_task_item(6).expect("remove box");
5277 assert_eq!(ed.source_str().unwrap(), "- a\n");
5278
5279 assert_eq!(ed.set_task_checked(2, true), Err(Error::NotEditable));
5282 let mut para = Editor::new_str("a\n", Format::Markdown).expect("editor");
5284 assert_eq!(para.toggle_task_item(0), Err(Error::NotFound));
5285 }
5286
5287 #[test]
5288 fn editor_insert_footnote_writes_both_halves_as_one_edit() {
5289 for format in [Format::Markdown, Format::Djot] {
5290 let mut ed = Editor::new_str("see\n", format).expect("editor");
5291 ed.insert_footnote(3, "a").expect("footnote");
5292 assert_eq!(ed.source_str().unwrap(), "see[^a]\n\n[^a]: \n");
5293
5294 let nodes = ed.nodes().expect("nodes");
5296 assert!(nodes.iter().any(|n| n.kind == Kind::FootnoteReference));
5297 assert!(nodes.iter().any(|n| n.kind == Kind::Footnote));
5298
5299 ed.undo().expect("undo");
5301 assert_eq!(ed.source_str().unwrap(), "see\n");
5302 }
5303 }
5304
5305 #[test]
5306 fn editor_insert_footnote_reuses_an_existing_definition() {
5307 let mut ed = Editor::new_str("see\n", Format::Markdown).expect("editor");
5308 ed.insert_footnote(3, "a").expect("first");
5309 ed.insert_footnote(7, "a").expect("second reference");
5310 assert_eq!(ed.source_str().unwrap(), "see[^a][^a]\n\n[^a]: \n");
5311 let defs = ed
5312 .nodes()
5313 .unwrap()
5314 .iter()
5315 .filter(|n| n.kind == Kind::Footnote)
5316 .count();
5317 assert_eq!(defs, 1);
5318
5319 assert_eq!(ed.insert_footnote(3, ""), Err(Error::InvalidArgument));
5320 assert_eq!(ed.insert_footnote(3, "a]b"), Err(Error::InvalidArgument));
5321 }
5322
5323 #[test]
5324 fn editor_undo_redo_round_trip() {
5325 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5326 ed.edit_range(5, 5, "!").expect("edit");
5327 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5328
5329 let change = ed.undo().expect("undo ok").expect("something to undo");
5330 assert_eq!(ed.source_str().unwrap(), "hello\n");
5331 assert_eq!(change.new.end, 5);
5332 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
5333
5334 ed.redo().expect("redo ok").expect("something to redo");
5335 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5336 }
5337
5338 #[test]
5339 fn editor_coalesce_folds_a_run() {
5340 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5341 ed.edit_range(0, 0, "a").expect("edit");
5342 ed.edit_range(1, 1, "b").expect("edit");
5343 ed.coalesce_last_undo().expect("coalesce");
5344 assert_eq!(ed.source_str().unwrap(), "ab\n");
5345 ed.undo().expect("undo ok").expect("something to undo");
5347 assert_eq!(ed.source_str().unwrap(), "\n");
5348 assert!(ed.undo().expect("undo ok").is_none());
5349 }
5350
5351 #[test]
5352 fn editor_revision_bumps_per_successful_mutation() {
5353 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
5354 assert_eq!(ed.revision(), 0);
5355 ed.edit_range(1, 1, "y").expect("edit");
5356 assert_eq!(ed.revision(), 1);
5357
5358 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
5360 assert_eq!(xml.revision(), 0);
5361 assert!(xml.replace_content("0", "<b>").is_err());
5362 assert_eq!(xml.revision(), 0);
5363
5364 ed.undo().expect("undo ok").expect("something to undo");
5366 assert_eq!(ed.revision(), 2);
5367 ed.redo().expect("redo ok").expect("something to redo");
5368 assert_eq!(ed.revision(), 3);
5369 }
5370
5371 #[test]
5372 fn editor_dirty_range_tracks_and_clears() {
5373 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
5374 assert_eq!(ed.dirty_range(), None);
5376
5377 ed.edit_range(2, 2, "XY").expect("edit");
5379 assert_eq!(ed.dirty_range(), Some(2..4));
5380
5381 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
5385 assert!(
5386 d.start <= 2 && d.end >= 10,
5387 "range {d:?} must cover both edits"
5388 );
5389
5390 let rev = ed.revision();
5392 ed.clear_dirty();
5393 assert_eq!(ed.dirty_range(), None);
5394 assert_eq!(ed.revision(), rev);
5395
5396 ed.undo().expect("undo ok").expect("something to undo");
5398 assert!(ed.dirty_range().is_some());
5399 }
5400
5401 #[test]
5402 fn editor_caret_blob_follows_undo_and_redo() {
5403 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5404 assert!(ed.caret_blob().unwrap().is_empty());
5405
5406 ed.set_caret_blob(b"before").expect("set caret");
5408 ed.edit_range(5, 5, "!").expect("edit");
5409 assert!(ed.caret_blob().unwrap().is_empty());
5411 ed.set_caret_blob(b"after").expect("set caret");
5412
5413 ed.undo().expect("undo ok").expect("something to undo");
5415 assert_eq!(ed.source_str().unwrap(), "hello\n");
5416 assert_eq!(ed.caret_blob().unwrap(), b"before");
5417
5418 ed.redo().expect("redo ok").expect("something to redo");
5420 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5421 assert_eq!(ed.caret_blob().unwrap(), b"after");
5422 }
5423
5424 #[test]
5425 fn editor_coalesced_run_keeps_the_pre_run_caret() {
5426 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5427 ed.set_caret_blob(b"c0").expect("set caret");
5428 ed.edit_range(0, 0, "a").expect("edit");
5429 ed.set_caret_blob(b"c1").expect("set caret");
5430 ed.edit_range(1, 1, "b").expect("edit");
5431 ed.coalesce_last_undo().expect("coalesce");
5432 ed.set_caret_blob(b"c2").expect("set caret");
5433
5434 ed.undo().expect("undo ok").expect("something to undo");
5436 assert_eq!(ed.source_str().unwrap(), "\n");
5437 assert_eq!(ed.caret_blob().unwrap(), b"c0");
5438 }
5439
5440 #[test]
5441 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
5442 let mut ed = Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
5443 ed.renumber_ordered_lists(0).expect("renumber ok");
5444 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
5445 }
5446
5447 #[test]
5448 fn editor_renumber_ordered_lists_leaves_djot_prose_alone() {
5449 let src = "1. a\n 2. b\n2. c\n";
5452 let mut dj = Editor::new_str(src, Format::Djot).expect("editor");
5453 dj.renumber_ordered_lists(0).expect("renumber ok");
5454 assert_eq!(dj.source_str().unwrap(), src);
5455
5456 let mut md = Editor::new_str(src, Format::Markdown).expect("editor");
5457 md.renumber_ordered_lists(0).expect("renumber ok");
5458 assert_eq!(md.source_str().unwrap(), "1. a\n 1. b\n2. c\n");
5459 }
5460
5461 #[test]
5462 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
5463 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
5464 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
5465 }
5466
5467 #[test]
5468 fn editor_table_insert_row_and_set_alignment() {
5469 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
5470 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
5471 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
5473 ed.source_str().unwrap(),
5474 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
5475 );
5476 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
5478 }
5479
5480 #[test]
5481 fn editor_table_edit_off_a_table_is_not_found() {
5482 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
5483 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
5484 }
5485
5486 #[test]
5487 fn editor_set_block_converts_setext_heading() {
5488 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
5490 ed.set_block(0, BlockKind::Heading(1))
5491 .expect("setext to atx");
5492 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
5493 }
5494
5495 #[test]
5496 fn editor_unwrap_and_smart_delete() {
5497 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
5498 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
5500
5501 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
5502 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
5504 }
5505
5506 #[test]
5507 fn editor_directives_require_the_extension_flag() {
5508 let src = ":::vis{.public}\nhi\n:::\n";
5509 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
5512 assert_eq!(plain.query("directive").expect("query").len(), 0);
5513 let mut ext = Editor::new_ext(
5515 src.as_bytes(),
5516 Format::Markdown,
5517 MarkdownExtensions {
5518 directives: true,
5519 ..Default::default()
5520 },
5521 )
5522 .expect("editor");
5523 assert_eq!(ext.query("directive").expect("query").len(), 1);
5524 }
5525
5526 #[test]
5527 fn document_html_elements_make_embedded_img_queryable() {
5528 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
5529 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
5531 assert_eq!(plain.query("image").expect("query").len(), 0);
5532 let mut ext = Document::parse_str_with(
5534 src,
5535 Format::Markdown,
5536 MarkdownExtensions {
5537 html_elements: true,
5538 ..Default::default()
5539 },
5540 )
5541 .expect("parse");
5542 let images = ext.query("image").expect("query");
5543 assert_eq!(images.len(), 1);
5544 assert_eq!(images[0].kind, Kind::Image);
5545 }
5546
5547 #[test]
5548 fn editor_filter_public_audience_view() {
5549 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
5550 let mut ed = Editor::new_ext(
5551 src.as_bytes(),
5552 Format::Markdown,
5553 MarkdownExtensions {
5554 directives: true,
5555 ..Default::default()
5556 },
5557 )
5558 .expect("editor");
5559 ed.filter(
5561 "directive[name=vis]",
5562 Some("directive[class~=public]"),
5563 true,
5564 )
5565 .expect("filter");
5566 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
5567 }
5568
5569 #[test]
5570 fn editor_filter_rejects_a_malformed_selector() {
5571 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
5572 assert_eq!(
5573 ed.filter("list >", None, false),
5574 Err(Error::InvalidArgument)
5575 );
5576 }
5577
5578 #[test]
5579 fn builder_builds_and_renders_a_document() {
5580 let mut b = Builder::new().expect("builder");
5581
5582 let title = b.add_text(TextKind::Str, "Title").unwrap();
5584 let heading = b.add_heading(1).unwrap();
5585 b.set_children(heading, &[title]).unwrap();
5586
5587 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
5588 let world = b.add_text(TextKind::Str, "world").unwrap();
5589 let emph = b.add(VoidKind::Emph).unwrap();
5590 b.set_children(emph, &[world]).unwrap();
5591 let para = b.add(VoidKind::Para).unwrap();
5592 b.set_children(para, &[hello, emph]).unwrap();
5593
5594 let doc = b.add(VoidKind::Doc).unwrap();
5595 b.set_children(doc, &[heading, para]).unwrap();
5596
5597 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
5598 assert!(html.contains("<h1>Title</h1>"), "{html}");
5599 assert!(html.contains("<em>world</em>"), "{html}");
5600
5601 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5602 assert!(md.contains("# Title"), "{md}");
5603 assert!(md.contains("*world*"), "{md}");
5604
5605 let matches = b.query(doc, "heading").unwrap();
5606 assert_eq!(matches.len(), 1);
5607 assert_eq!(matches[0].kind, Kind::Heading);
5608
5609 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
5610 assert!(json.contains("\"kind\": \"doc\""), "{json}");
5611 }
5612
5613 #[test]
5614 fn builder_element_with_attributes() {
5615 let mut b = Builder::new().expect("builder");
5616 let inner = b.add_text(TextKind::Str, "hi").unwrap();
5617 let el = b.add_element("section").unwrap();
5618 b.set_children(el, &[inner]).unwrap();
5619 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)])
5620 .unwrap();
5621
5622 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
5623 assert!(html.contains("<section"), "{html}");
5624 assert!(html.contains("class=\"note\""), "{html}");
5625 assert!(html.contains("hidden"), "{html}");
5626 }
5627
5628 #[test]
5629 fn builder_lists_round_trip_to_markdown() {
5630 let mut b = Builder::new().expect("builder");
5631
5632 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
5634 let one_para = b.add(VoidKind::Para).unwrap();
5635 b.set_children(one_para, &[one_txt]).unwrap();
5636 let one = b.add(VoidKind::ListItem).unwrap();
5637 b.set_children(one, &[one_para]).unwrap();
5638
5639 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
5640 let two_para = b.add(VoidKind::Para).unwrap();
5641 b.set_children(two_para, &[two_txt]).unwrap();
5642 let two = b.add(VoidKind::ListItem).unwrap();
5643 b.set_children(two, &[two_para]).unwrap();
5644
5645 let list = b
5646 .add_ordered_list(
5647 OrderedNumbering::Decimal,
5648 OrderedDelim::Period,
5649 true,
5650 Some(1),
5651 )
5652 .unwrap();
5653 b.set_children(list, &[one, two]).unwrap();
5654 let doc = b.add(VoidKind::Doc).unwrap();
5655 b.set_children(doc, &[list]).unwrap();
5656
5657 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5658 assert!(md.contains("1. one"), "{md}");
5659 assert!(md.contains("2. two"), "{md}");
5660 }
5661
5662 #[test]
5663 fn builder_rejects_invalid_kind_and_id() {
5664 let b = Builder::new().expect("builder");
5665 let mut id = 0u32;
5669 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
5670 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5671
5672 let mut ptr = std::ptr::null();
5674 let mut len = 0usize;
5675 let status =
5676 unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
5677 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5678 }
5679
5680 fn all_gestures() -> Vec<Gesture> {
5684 let inline = [
5685 InlineKind::Strong,
5686 InlineKind::Emph,
5687 InlineKind::Verbatim,
5688 InlineKind::Mark,
5689 InlineKind::Superscript,
5690 InlineKind::Subscript,
5691 InlineKind::Insert,
5692 InlineKind::Delete,
5693 ];
5694 let mut all: Vec<Gesture> = Vec::new();
5695 for k in inline {
5696 all.push(Gesture::WrapRange(k));
5697 all.push(Gesture::ToggleInline(k));
5698 }
5699 for k in [
5700 BlockContainerKind::BlockQuote,
5701 BlockContainerKind::BulletList,
5702 BlockContainerKind::OrderedList,
5703 ] {
5704 all.push(Gesture::ToggleBlockContainer(k));
5705 }
5706 all.extend([
5707 Gesture::SetMarkColor,
5708 Gesture::SetBlock,
5709 Gesture::InsertThematicBreak,
5710 Gesture::ToggleCodeBlock,
5711 Gesture::SetCodeLanguage,
5712 Gesture::ToggleTaskItem,
5713 Gesture::SetTaskChecked,
5714 Gesture::ToggleTaskChecked,
5715 Gesture::InsertLink,
5716 Gesture::InsertImage,
5717 Gesture::InsertFootnote,
5718 Gesture::InsertLiteral,
5719 Gesture::InsertLineBreak,
5720 Gesture::SplitBlock,
5721 Gesture::RenumberOrderedLists,
5722 Gesture::TableInsertRow,
5723 Gesture::TableDeleteRow,
5724 Gesture::TableInsertColumn,
5725 Gesture::TableDeleteColumn,
5726 Gesture::TableSetAlignment,
5727 Gesture::TableMoveRow,
5728 Gesture::TableMoveColumn,
5729 ]);
5730 all
5731 }
5732
5733 #[test]
5734 fn the_wire_space_ends_where_the_sweep_does() {
5735 let mut codes: Vec<c_int> = all_gestures().iter().map(|g| g.to_c().0).collect();
5741 codes.sort_unstable();
5742 codes.dedup();
5743 assert_eq!(codes, (0..=24).collect::<Vec<c_int>>());
5744
5745 let mut supported = -1;
5746 for code in &codes {
5747 let status = unsafe {
5748 ffi::twig_format_supports(
5749 ffi::TwigFormat::from(Format::Markdown) as c_int,
5750 *code,
5751 0,
5752 &mut supported,
5753 )
5754 };
5755 assert_eq!(Error::from_status(status), Ok(()), "code {code} did not decode");
5756 }
5757 let status = unsafe {
5759 ffi::twig_format_supports(
5760 ffi::TwigFormat::from(Format::Markdown) as c_int,
5761 25,
5762 0,
5763 &mut supported,
5764 )
5765 };
5766 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5767 }
5768
5769 #[test]
5770 fn supports_answers_per_gesture_where_authorable_cannot() {
5771 assert!(Format::Html.is_authorable());
5776 assert!(Format::Html.supports(Gesture::ToggleInline(InlineKind::Strong)));
5777 assert!(!Format::Html.supports(Gesture::SetBlock));
5778 assert!(!Format::Html.supports(Gesture::ToggleBlockContainer(
5779 BlockContainerKind::BlockQuote
5780 )));
5781 assert!(!Format::Html.supports(Gesture::ToggleCodeBlock));
5782 assert!(!Format::Html.supports(Gesture::InsertLiteral));
5783 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5787 assert!(!Format::Html.supports(Gesture::TableSetAlignment));
5788 assert!(!Format::Html.supports(Gesture::SplitBlock));
5789 assert!(!Format::Html.supports(Gesture::RenumberOrderedLists));
5790 assert!(Format::Markdown.supports(Gesture::TableInsertRow));
5791 assert!(Format::Djot.supports(Gesture::SplitBlock));
5792
5793 for fmt in [Format::Xml] {
5796 assert!(!fmt.is_authorable());
5797 for g in all_gestures() {
5798 assert!(!fmt.supports(g), "{fmt:?} claims to spell {g:?}");
5799 }
5800 }
5801 assert!(Format::Asciidoc.is_authorable());
5804 assert!(Format::Asciidoc.supports(Gesture::SetBlock));
5805 assert!(Format::Asciidoc.supports(Gesture::ToggleInline(InlineKind::Mark)));
5806 assert!(!Format::Asciidoc.supports(Gesture::InsertLink));
5807 assert!(!Format::Asciidoc.supports(Gesture::TableInsertRow));
5808
5809 assert!(Format::Djot.supports(Gesture::ToggleInline(InlineKind::Mark)));
5812 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
5813 assert!(Format::Markdown.supports(Gesture::InsertLineBreak));
5814 assert!(!Format::Djot.supports(Gesture::InsertLineBreak));
5815 }
5816
5817 #[test]
5818 fn supports_agrees_with_what_the_editor_then_does() {
5819 for fmt in [Format::Djot, Format::Markdown, Format::Html] {
5824 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5825 let claimed = fmt.supports(Gesture::ToggleInline(InlineKind::Mark));
5826 let observed = ed.toggle_inline(0, 2, InlineKind::Mark);
5827 assert_eq!(
5828 claimed,
5829 !matches!(observed, Err(Error::UnsupportedFormat)),
5830 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5831 );
5832
5833 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5834 let claimed = fmt.supports(Gesture::SetBlock);
5835 let observed = ed.set_block(0, BlockKind::Heading(1));
5836 assert_eq!(
5837 claimed,
5838 !matches!(observed, Err(Error::UnsupportedFormat)),
5839 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5840 );
5841 }
5842
5843 let src = "<table><tr><td>a</td></tr></table>";
5847 let mut ed = Editor::new_str(src, Format::Html).expect("editor");
5848 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5849 assert_eq!(ed.table_insert_row(15, true), Err(Error::UnsupportedFormat));
5850 assert_eq!(ed.renumber_ordered_lists(15), Err(Error::UnsupportedFormat));
5851 assert!(matches!(ed.split_block(15), Err(Error::UnsupportedFormat)));
5852 assert_eq!(ed.source().expect("source"), src.as_bytes());
5853 }
5854
5855 #[test]
5856 fn supports_rides_the_gestures_own_kind_space() {
5857 let (g, k) = Gesture::ToggleBlockContainer(BlockContainerKind::BulletList).to_c();
5862 assert_eq!((g, k), (3, 1));
5863 let (g, k) = Gesture::ToggleInline(InlineKind::Emph).to_c();
5864 assert_eq!((g, k), (1, 1));
5865 assert_eq!(Gesture::InsertLink.to_c(), (10, 0));
5868
5869 let mut out: c_int = 0;
5871 let status = unsafe {
5872 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 10, 3, &mut out)
5873 };
5874 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5875 let status = unsafe {
5876 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 9999, 0, &mut out)
5877 };
5878 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5879 }
5880}