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 Commonmark,
51 Gfm,
57}
58
59impl From<Format> for ffi::TwigFormat {
60 fn from(value: Format) -> Self {
61 match value {
62 Format::Djot => ffi::TwigFormat::Djot,
63 Format::Markdown => ffi::TwigFormat::Markdown,
64 Format::Xml => ffi::TwigFormat::Xml,
65 Format::Html => ffi::TwigFormat::Html,
66 Format::Asciidoc => ffi::TwigFormat::Asciidoc,
67 Format::Commonmark => ffi::TwigFormat::Commonmark,
68 Format::Gfm => ffi::TwigFormat::Gfm,
69 }
70 }
71}
72
73impl Format {
74 pub fn dialect_of(self) -> Option<Format> {
81 match self {
82 Format::Commonmark | Format::Gfm => Some(Format::Markdown),
83 _ => None,
84 }
85 }
86}
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103#[non_exhaustive]
104pub enum Target {
105 Djot,
106 Markdown,
107 Xml,
108 Html,
109 Asciidoc,
113}
114
115impl Target {
116 pub fn as_format(self) -> Option<Format> {
123 match self {
124 Target::Djot => Some(Format::Djot),
125 Target::Markdown => Some(Format::Markdown),
126 Target::Xml => Some(Format::Xml),
127 Target::Html => Some(Format::Html),
128 Target::Asciidoc => Some(Format::Asciidoc),
129 }
130 }
131}
132
133impl From<Format> for Target {
139 fn from(value: Format) -> Self {
140 match value {
141 Format::Djot => Target::Djot,
142 Format::Markdown | Format::Commonmark | Format::Gfm => Target::Markdown,
143 Format::Xml => Target::Xml,
144 Format::Html => Target::Html,
145 Format::Asciidoc => Target::Asciidoc,
146 }
147 }
148}
149
150impl From<Target> for ffi::TwigFormat {
151 fn from(value: Target) -> Self {
152 match value {
153 Target::Djot => ffi::TwigFormat::Djot,
154 Target::Markdown => ffi::TwigFormat::Markdown,
155 Target::Xml => ffi::TwigFormat::Xml,
156 Target::Html => ffi::TwigFormat::Html,
157 Target::Asciidoc => ffi::TwigFormat::Asciidoc,
158 }
159 }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq, Hash)]
196#[non_exhaustive]
197pub enum Kind {
198 Doc,
200 Para,
202 Heading,
203 ThematicBreak,
204 Section,
205 CodeBlock,
206 RawBlock,
207 Metadata,
208 BlockQuote,
209 BulletList,
210 OrderedList,
211 TaskList,
212 DefinitionList,
213 LineBlock,
214 Table,
215 ListItem,
217 TaskListItem,
218 DefinitionListItem,
219 Term,
220 Definition,
221 Line,
222 Row,
223 Cell,
224 Column,
225 Caption,
226 Footnote,
227 Reference,
228 Citation,
229 Substitution,
230 Str,
232 SoftBreak,
233 HardBreak,
234 NonBreakingSpace,
235 RawInline,
236 SmartPunctuation,
237 Link,
238 Image,
239 Emph,
241 Strong,
242 Mark,
243 Superscript,
244 Subscript,
245 Insert,
246 Delete,
247 DoubleQuoted,
248 SingleQuoted,
249 Symb,
251 Verbatim,
252 InlineMath,
253 DisplayMath,
254 Url,
255 Email,
256 FootnoteReference,
257 CitationReference,
258 SubstitutionReference,
259 Container,
261 ProcessingInstruction,
262 Comment,
263 Doctype,
264 Cdata,
265 Other(String),
272}
273
274impl Kind {
275 pub fn as_str(&self) -> &str {
278 match self {
279 Kind::Doc => "doc",
280 Kind::Para => "para",
281 Kind::Heading => "heading",
282 Kind::ThematicBreak => "thematic_break",
283 Kind::Section => "section",
284 Kind::CodeBlock => "code_block",
285 Kind::RawBlock => "raw_block",
286 Kind::Metadata => "metadata",
287 Kind::BlockQuote => "block_quote",
288 Kind::BulletList => "bullet_list",
289 Kind::OrderedList => "ordered_list",
290 Kind::TaskList => "task_list",
291 Kind::DefinitionList => "definition_list",
292 Kind::LineBlock => "line_block",
293 Kind::Table => "table",
294 Kind::ListItem => "list_item",
295 Kind::TaskListItem => "task_list_item",
296 Kind::DefinitionListItem => "definition_list_item",
297 Kind::Term => "term",
298 Kind::Definition => "definition",
299 Kind::Line => "line",
300 Kind::Row => "row",
301 Kind::Cell => "cell",
302 Kind::Column => "column",
303 Kind::Caption => "caption",
304 Kind::Footnote => "footnote",
305 Kind::Reference => "reference",
306 Kind::Citation => "citation",
307 Kind::Substitution => "substitution",
308 Kind::Str => "str",
309 Kind::SoftBreak => "soft_break",
310 Kind::HardBreak => "hard_break",
311 Kind::NonBreakingSpace => "non_breaking_space",
312 Kind::RawInline => "raw_inline",
313 Kind::SmartPunctuation => "smart_punctuation",
314 Kind::Link => "link",
315 Kind::Image => "image",
316 Kind::Container => "container",
317 Kind::ProcessingInstruction => "processing_instruction",
318 Kind::Emph => "emph",
319 Kind::Strong => "strong",
320 Kind::Mark => "mark",
321 Kind::Superscript => "superscript",
322 Kind::Subscript => "subscript",
323 Kind::Insert => "insert",
324 Kind::Delete => "delete",
325 Kind::DoubleQuoted => "double_quoted",
326 Kind::SingleQuoted => "single_quoted",
327 Kind::Symb => "symb",
328 Kind::Verbatim => "verbatim",
329 Kind::InlineMath => "inline_math",
330 Kind::DisplayMath => "display_math",
331 Kind::Url => "url",
332 Kind::Email => "email",
333 Kind::FootnoteReference => "footnote_reference",
334 Kind::CitationReference => "citation_reference",
335 Kind::SubstitutionReference => "substitution_reference",
336 Kind::Comment => "comment",
337 Kind::Doctype => "doctype",
338 Kind::Cdata => "cdata",
339 Kind::Other(name) => name.as_str(),
340 }
341 }
342
343 pub fn is_unknown(&self) -> bool {
347 matches!(self, Kind::Other(_))
348 }
349}
350
351impl From<&str> for Kind {
352 fn from(name: &str) -> Self {
353 match name {
354 "doc" => Kind::Doc,
355 "para" => Kind::Para,
356 "heading" => Kind::Heading,
357 "thematic_break" => Kind::ThematicBreak,
358 "section" => Kind::Section,
359 "code_block" => Kind::CodeBlock,
360 "raw_block" => Kind::RawBlock,
361 "metadata" => Kind::Metadata,
362 "block_quote" => Kind::BlockQuote,
363 "bullet_list" => Kind::BulletList,
364 "ordered_list" => Kind::OrderedList,
365 "task_list" => Kind::TaskList,
366 "definition_list" => Kind::DefinitionList,
367 "line_block" => Kind::LineBlock,
368 "table" => Kind::Table,
369 "list_item" => Kind::ListItem,
370 "task_list_item" => Kind::TaskListItem,
371 "definition_list_item" => Kind::DefinitionListItem,
372 "term" => Kind::Term,
373 "definition" => Kind::Definition,
374 "line" => Kind::Line,
375 "row" => Kind::Row,
376 "cell" => Kind::Cell,
377 "column" => Kind::Column,
378 "caption" => Kind::Caption,
379 "footnote" => Kind::Footnote,
380 "reference" => Kind::Reference,
381 "citation" => Kind::Citation,
382 "substitution" => Kind::Substitution,
383 "str" => Kind::Str,
384 "soft_break" => Kind::SoftBreak,
385 "hard_break" => Kind::HardBreak,
386 "non_breaking_space" => Kind::NonBreakingSpace,
387 "raw_inline" => Kind::RawInline,
388 "smart_punctuation" => Kind::SmartPunctuation,
389 "link" => Kind::Link,
390 "image" => Kind::Image,
391 "container" => Kind::Container,
392 "processing_instruction" => Kind::ProcessingInstruction,
393 "emph" => Kind::Emph,
394 "strong" => Kind::Strong,
395 "mark" => Kind::Mark,
396 "superscript" => Kind::Superscript,
397 "subscript" => Kind::Subscript,
398 "insert" => Kind::Insert,
399 "delete" => Kind::Delete,
400 "double_quoted" => Kind::DoubleQuoted,
401 "single_quoted" => Kind::SingleQuoted,
402 "symb" => Kind::Symb,
403 "verbatim" => Kind::Verbatim,
404 "inline_math" => Kind::InlineMath,
405 "display_math" => Kind::DisplayMath,
406 "url" => Kind::Url,
407 "email" => Kind::Email,
408 "footnote_reference" => Kind::FootnoteReference,
409 "citation_reference" => Kind::CitationReference,
410 "substitution_reference" => Kind::SubstitutionReference,
411 "comment" => Kind::Comment,
412 "doctype" => Kind::Doctype,
413 "cdata" => Kind::Cdata,
414 other => Kind::Other(other.to_string()),
415 }
416 }
417}
418
419impl std::fmt::Display for Kind {
420 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421 f.write_str(self.as_str())
422 }
423}
424
425#[derive(Clone, Debug, Eq, PartialEq)]
427pub struct QueryMatch {
428 pub node_id: u32,
430 pub span: Range<usize>,
432 pub content_span: Option<Range<usize>>,
435 pub kind: Kind,
438}
439
440#[derive(Clone, Debug, Eq, PartialEq)]
447pub struct Change {
448 pub old: Range<usize>,
449 pub new: Range<usize>,
450}
451
452impl Change {
453 pub fn delta(&self) -> isize {
455 self.new.len() as isize - self.old.len() as isize
456 }
457
458 fn from_ffi(c: ffi::TwigChange) -> Self {
459 Change {
460 old: c.old_span.start..c.old_span.end,
461 new: c.new_span.start..c.new_span.end,
462 }
463 }
464}
465
466#[derive(Clone, Debug, Eq, PartialEq)]
477#[non_exhaustive]
478pub struct FlatNode {
479 pub id: NodeId,
480 pub parent: Option<NodeId>,
481 pub first_child: Option<NodeId>,
482 pub next_sibling: Option<NodeId>,
483 pub span: Range<usize>,
484 pub content_span: Option<Range<usize>>,
485 pub level: Option<u32>,
487 pub kind: Kind,
488 pub text: Option<String>,
489 pub destination: Option<String>,
490 pub head: Option<bool>,
493 pub alignment: Option<Alignment>,
499 pub name: Option<String>,
511 pub directive_form: Option<DirectiveForm>,
526 pub origin: Option<ContainerOrigin>,
536 pub marker_span: Option<Range<usize>>,
555 pub checked: Option<bool>,
568 pub attrs: Vec<(String, Option<String>)>,
572}
573
574#[derive(Clone, Debug, Default, Eq, PartialEq)]
582pub struct LinePrefix {
583 pub text: String,
585 pub columns: usize,
587}
588
589#[derive(Clone, Copy, Debug, Eq, PartialEq)]
599pub enum InlineKind {
600 Strong,
601 Emph,
602 Verbatim,
603 Mark,
604 Superscript,
605 Subscript,
606 Insert,
607 Delete,
608}
609
610impl InlineKind {
611 fn to_c(self) -> c_int {
612 match self {
613 InlineKind::Strong => 0,
614 InlineKind::Emph => 1,
615 InlineKind::Verbatim => 2,
616 InlineKind::Mark => 3,
617 InlineKind::Superscript => 4,
618 InlineKind::Subscript => 5,
619 InlineKind::Insert => 6,
620 InlineKind::Delete => 7,
621 }
622 }
623}
624
625#[derive(Clone, Copy, Debug, Eq, PartialEq)]
627pub enum BlockKind {
628 Paragraph,
629 Heading(u32),
631}
632
633impl BlockKind {
634 fn to_c(self) -> (c_int, u32) {
636 match self {
637 BlockKind::Paragraph => (0, 0),
638 BlockKind::Heading(level) => (1, level),
639 }
640 }
641}
642
643#[derive(Clone, Copy, Debug, Eq, PartialEq)]
649pub enum BlockContainerKind {
650 BlockQuote,
651 BulletList,
652 OrderedList,
653}
654
655impl BlockContainerKind {
656 fn to_c(self) -> c_int {
657 match self {
658 BlockContainerKind::BlockQuote => 0,
659 BlockContainerKind::BulletList => 1,
660 BlockContainerKind::OrderedList => 2,
661 }
662 }
663}
664
665#[derive(Clone, Copy, Debug, Eq, PartialEq)]
679pub enum MarkColor {
680 Red,
681 Orange,
682 Yellow,
683 Green,
684 Blue,
685 Purple,
686 Brown,
687}
688
689impl MarkColor {
690 pub fn as_str(self) -> &'static str {
692 match self {
693 MarkColor::Red => "red",
694 MarkColor::Orange => "orange",
695 MarkColor::Yellow => "yellow",
696 MarkColor::Green => "green",
697 MarkColor::Blue => "blue",
698 MarkColor::Purple => "purple",
699 MarkColor::Brown => "brown",
700 }
701 }
702
703 pub fn from_str(s: &str) -> Option<Self> {
706 Some(match s {
707 "red" => MarkColor::Red,
708 "orange" => MarkColor::Orange,
709 "yellow" => MarkColor::Yellow,
710 "green" => MarkColor::Green,
711 "blue" => MarkColor::Blue,
712 "purple" => MarkColor::Purple,
713 "brown" => MarkColor::Brown,
714 _ => return None,
715 })
716 }
717}
718
719#[derive(Clone, Copy, Debug, Eq, PartialEq)]
759#[non_exhaustive]
760pub enum Gesture {
761 WrapRange(InlineKind),
762 ToggleInline(InlineKind),
763 SetBlock,
764 ToggleBlockContainer(BlockContainerKind),
765 InsertThematicBreak,
766 ToggleCodeBlock,
767 SetCodeLanguage,
768 ToggleTaskItem,
769 SetTaskChecked,
770 ToggleTaskChecked,
771 InsertLink,
772 InsertImage,
773 InsertFootnote,
774 InsertLiteral,
775 InsertLineBreak,
776 SplitBlock,
777 RenumberOrderedLists,
778 TableInsertRow,
779 TableDeleteRow,
780 TableInsertColumn,
781 TableDeleteColumn,
782 TableSetAlignment,
783 TableMoveRow,
784 TableMoveColumn,
785 SetMarkColor,
793}
794
795impl Gesture {
796 fn to_c(self) -> (c_int, c_int) {
801 match self {
802 Gesture::WrapRange(k) => (0, k.to_c()),
803 Gesture::ToggleInline(k) => (1, k.to_c()),
804 Gesture::SetBlock => (2, 0),
805 Gesture::ToggleBlockContainer(k) => (3, k.to_c()),
806 Gesture::InsertThematicBreak => (4, 0),
807 Gesture::ToggleCodeBlock => (5, 0),
808 Gesture::SetCodeLanguage => (6, 0),
809 Gesture::ToggleTaskItem => (7, 0),
810 Gesture::SetTaskChecked => (8, 0),
811 Gesture::ToggleTaskChecked => (9, 0),
812 Gesture::InsertLink => (10, 0),
813 Gesture::InsertImage => (11, 0),
814 Gesture::InsertFootnote => (12, 0),
815 Gesture::InsertLiteral => (13, 0),
816 Gesture::InsertLineBreak => (14, 0),
817 Gesture::SplitBlock => (15, 0),
818 Gesture::RenumberOrderedLists => (16, 0),
819 Gesture::TableInsertRow => (17, 0),
820 Gesture::TableDeleteRow => (18, 0),
821 Gesture::TableInsertColumn => (19, 0),
822 Gesture::TableDeleteColumn => (20, 0),
823 Gesture::TableSetAlignment => (21, 0),
824 Gesture::TableMoveRow => (22, 0),
825 Gesture::TableMoveColumn => (23, 0),
826 Gesture::SetMarkColor => (24, 0),
827 }
828 }
829}
830
831impl Format {
832 pub fn supports(self, gesture: Gesture) -> bool {
857 let (g, k) = gesture.to_c();
858 let mut supported: c_int = 0;
859 let status = unsafe {
860 ffi::twig_format_supports(ffi::TwigFormat::from(self) as c_int, g, k, &mut supported)
861 };
862 debug_assert!(
863 Error::from_status(status).is_ok(),
864 "twig_format_supports rejected a combination the Rust types make unrepresentable",
865 );
866 supported == 1
867 }
868
869 pub fn supports_with(self, extensions: MarkdownExtensions, gesture: Gesture) -> bool {
893 let (g, k) = gesture.to_c();
894 let mut supported: c_int = 0;
895 let status = unsafe {
896 ffi::twig_format_supports_ext(
897 ffi::TwigFormat::from(self) as c_int,
898 extensions.to_flags(),
899 g,
900 k,
901 &mut supported,
902 )
903 };
904 debug_assert!(
905 Error::from_status(status).is_ok(),
906 "twig_format_supports_ext rejected a combination the Rust types make unrepresentable",
907 );
908 supported == 1
909 }
910
911 pub fn is_authorable(self) -> bool {
921 let mut authorable: c_int = 0;
922 let status = unsafe {
923 ffi::twig_format_is_authorable(ffi::TwigFormat::from(self) as c_int, &mut authorable)
924 };
925 debug_assert!(Error::from_status(status).is_ok(), "unknown format code");
926 authorable == 1
927 }
928}
929
930#[derive(Clone, Copy, Debug, Eq, PartialEq)]
931pub struct Version {
932 pub major: u8,
933 pub minor: u8,
934 pub patch: u8,
935}
936
937pub fn version() -> Version {
938 let packed = unsafe { ffi::twig_version() };
939 Version {
940 major: (packed >> 16) as u8,
941 minor: (packed >> 8) as u8,
942 patch: packed as u8,
943 }
944}
945
946pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
952
953pub fn abi_version() -> u32 {
959 unsafe { ffi::twig_abi_version() }
960}
961
962pub fn version_string() -> &'static str {
963 let ptr = unsafe { ffi::twig_version_string() };
964 unsafe { std::ffi::CStr::from_ptr(ptr) }
965 .to_str()
966 .unwrap_or("")
967}
968
969#[derive(Debug)]
970pub struct Document {
971 raw: NonNull<ffi::TwigDocument>,
972}
973
974impl Document {
975 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
976 Self::parse_with(input, format, MarkdownExtensions::default())
977 }
978
979 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
980 Self::parse(input.as_bytes(), format)
981 }
982
983 pub fn parse_with(
989 input: &[u8],
990 format: Format,
991 extensions: MarkdownExtensions,
992 ) -> Result<Self, Error> {
993 let mut raw = std::ptr::null_mut();
994 let ffi_format: ffi::TwigFormat = format.into();
995 let status = unsafe {
996 ffi::twig_parse_ext(
997 input.as_ptr(),
998 input.len(),
999 ffi_format as i32,
1000 extensions.to_flags(),
1001 &mut raw,
1002 )
1003 };
1004 Error::from_status(status)?;
1005 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1006 Ok(Self { raw })
1007 }
1008
1009 pub fn parse_str_with(
1011 input: &str,
1012 format: Format,
1013 extensions: MarkdownExtensions,
1014 ) -> Result<Self, Error> {
1015 Self::parse_with(input.as_bytes(), format, extensions)
1016 }
1017
1018 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
1021 let raw = self.raw.as_ptr();
1022 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
1023 }
1024
1025 pub fn serialize_to(&mut self, target: Target) -> Result<Vec<u8>, Error> {
1036 let raw = self.raw.as_ptr();
1037 let ffi_target: ffi::TwigFormat = target.into();
1038 collect_bytes(|ptr, len| unsafe {
1039 ffi::twig_document_serialize(raw, ffi_target as i32, ptr, len)
1040 })
1041 }
1042
1043 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
1050 self.serialize_to(format.into())
1051 }
1052
1053 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
1056 let raw = self.raw.as_ptr();
1057 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
1058 }
1059
1060 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1069 let raw = self.raw.as_ptr();
1070 collect_matches(|ptr, len| unsafe {
1071 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
1072 })
1073 }
1074
1075 pub fn span(&mut self, node: NodeId) -> Result<Range<usize>, Error> {
1077 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1078 let status = unsafe { ffi::twig_document_node_span(self.raw.as_ptr(), node.0, &mut span) };
1079 Error::from_status(status)?;
1080 Ok(span.start..span.end)
1081 }
1082
1083 pub fn content_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
1086 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1087 let status =
1088 unsafe { ffi::twig_document_node_content_span(self.raw.as_ptr(), node.0, &mut span) };
1089 match status.0 {
1090 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1091 ffi::TwigStatus::NOT_FOUND => Ok(None),
1092 _ => Err(Error::from_status(status).unwrap_err()),
1093 }
1094 }
1095
1096 pub fn marker_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
1100 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1101 let status =
1102 unsafe { ffi::twig_document_node_marker_span(self.raw.as_ptr(), node.0, &mut span) };
1103 match status.0 {
1104 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1105 ffi::TwigStatus::NOT_FOUND => Ok(None),
1106 _ => Err(Error::from_status(status).unwrap_err()),
1107 }
1108 }
1109
1110 pub fn attrs_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
1125 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1126 let status =
1127 unsafe { ffi::twig_document_attrs_span(self.raw.as_ptr(), node.0, &mut span) };
1128 match status.0 {
1129 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1130 ffi::TwigStatus::NOT_FOUND => Ok(None),
1131 _ => Err(Error::from_status(status).unwrap_err()),
1132 }
1133 }
1134
1135 pub fn line_prefix(&mut self, offset: usize) -> Result<Option<Range<usize>>, Error> {
1152 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1153 let status =
1154 unsafe { ffi::twig_document_line_prefix(self.raw.as_ptr(), offset, &mut span) };
1155 match status.0 {
1156 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1157 ffi::TwigStatus::NOT_FOUND => Ok(None),
1158 _ => Err(Error::from_status(status).unwrap_err()),
1159 }
1160 }
1161
1162 pub fn continuation_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1188 self.prefix_via(offset, ffi::twig_document_continuation_prefix)
1189 }
1190
1191 pub fn blank_line_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1203 self.prefix_via(offset, ffi::twig_document_blank_line_prefix)
1204 }
1205
1206 fn prefix_via(
1208 &mut self,
1209 offset: usize,
1210 f: unsafe extern "C" fn(
1211 *mut ffi::TwigDocument,
1212 usize,
1213 *mut *const u8,
1214 *mut usize,
1215 *mut usize,
1216 ) -> ffi::TwigStatus,
1217 ) -> Result<LinePrefix, Error> {
1218 let mut ptr: *const u8 = std::ptr::null();
1219 let mut len = 0usize;
1220 let mut columns = 0usize;
1221 let status = unsafe { f(self.raw.as_ptr(), offset, &mut ptr, &mut len, &mut columns) };
1222 Error::from_status(status)?;
1223 let text = if ptr.is_null() || len == 0 {
1224 String::new()
1225 } else {
1226 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1227 String::from_utf8(bytes.to_vec()).map_err(|_| Error::Internal)?
1228 };
1229 Ok(LinePrefix { text, columns })
1230 }
1231
1232 pub fn cell_extent(&mut self, node: NodeId) -> Result<Option<(u32, u32)>, Error> {
1244 let raw = self.raw.as_ptr();
1245 let mut colspan: u32 = 0;
1246 let status = unsafe { ffi::twig_document_cell_colspan(raw, node.0, &mut colspan) };
1247 match status.0 {
1248 ffi::TwigStatus::OK => {}
1249 ffi::TwigStatus::NOT_FOUND => return Ok(None),
1250 _ => return Err(Error::from_status(status).unwrap_err()),
1251 }
1252 let mut rowspan: u32 = 0;
1253 Error::from_status(unsafe { ffi::twig_document_cell_rowspan(raw, node.0, &mut rowspan) })?;
1254 Ok(Some((colspan, rowspan)))
1255 }
1256
1257 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1262 let raw = self.raw.as_ptr();
1263 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_nodes(raw, ptr, len) })
1264 }
1265
1266 pub fn definitions(&mut self) -> Result<Vec<QueryMatch>, Error> {
1283 let raw = self.raw.as_ptr();
1284 collect_matches(|ptr, len| unsafe { ffi::twig_document_definitions(raw, ptr, len) })
1285 }
1286
1287 pub fn diagnostics(&mut self, target: Target) -> Result<Vec<Warning>, Error> {
1307 let raw = self.raw.as_ptr();
1308 let code = ffi::TwigFormat::from(target) as c_int;
1309 let mut ptr: *const ffi::TwigWarning = std::ptr::null();
1310 let mut len = 0usize;
1311 let status = unsafe { ffi::twig_document_diagnostics(raw, code, &mut ptr, &mut len) };
1312 Error::from_status(status)?;
1313 if len == 0 || ptr.is_null() {
1314 return Ok(Vec::new());
1315 }
1316 let raw_warnings = unsafe { std::slice::from_raw_parts(ptr, len) };
1317 Ok(raw_warnings
1318 .iter()
1319 .map(|w| Warning {
1320 fidelity: Fidelity::from_c(w.fidelity),
1321 path: borrowed_bytes(w.path_ptr, w.path_len).unwrap_or_default(),
1322 kind: Kind::from(borrowed_cstr(w.kind).unwrap_or_default().as_str()),
1323 })
1324 .collect())
1325 }
1326
1327 pub fn children(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1333 let raw = self.raw.as_ptr();
1334 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1335 collect_matches(|ptr, len| unsafe { ffi::twig_document_children(raw, id, ptr, len) })
1336 }
1337
1338 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1344 let raw = self.raw.as_ptr();
1345 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_subtree(raw, node.0, ptr, len) })
1346 }
1347
1348 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1353 let mut m = empty_ffi_match();
1354 let status = unsafe { ffi::twig_document_node_at(self.raw.as_ptr(), offset, &mut m) };
1355 match status.0 {
1356 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1357 ffi::TwigStatus::NOT_FOUND => Ok(None),
1358 _ => Err(Error::from_status(status).unwrap_err()),
1359 }
1360 }
1361
1362 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1366 let raw = self.raw.as_ptr();
1367 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1368 let mut len = 0usize;
1369 let status = unsafe { ffi::twig_document_nodes_at(raw, offset, &mut ptr, &mut len) };
1370 match status.0 {
1371 ffi::TwigStatus::OK => {}
1372 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1373 _ => return Err(Error::from_status(status).unwrap_err()),
1374 }
1375 if len == 0 || ptr.is_null() {
1376 return Ok(Vec::new());
1377 }
1378 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1379 raw_matches.iter().map(query_match_from_ffi).collect()
1380 }
1381
1382 pub fn node_at_caret(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1405 let mut m = empty_ffi_match();
1406 let status = unsafe { ffi::twig_document_node_at_caret(self.raw.as_ptr(), offset, &mut m) };
1407 match status.0 {
1408 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1409 ffi::TwigStatus::NOT_FOUND => Ok(None),
1410 _ => Err(Error::from_status(status).unwrap_err()),
1411 }
1412 }
1413
1414 pub fn ancestors_at_caret(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1418 let raw = self.raw.as_ptr();
1419 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1420 let mut len = 0usize;
1421 let status = unsafe { ffi::twig_document_nodes_at_caret(raw, offset, &mut ptr, &mut len) };
1422 match status.0 {
1423 ffi::TwigStatus::OK => {}
1424 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1425 _ => return Err(Error::from_status(status).unwrap_err()),
1426 }
1427 if len == 0 || ptr.is_null() {
1428 return Ok(Vec::new());
1429 }
1430 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1431 raw_matches.iter().map(query_match_from_ffi).collect()
1432 }
1433}
1434
1435#[derive(Debug)]
1446pub struct DocumentView<'a> {
1447 doc: Document,
1448 _editor: PhantomData<&'a mut Editor>,
1449}
1450
1451impl std::ops::Deref for DocumentView<'_> {
1452 type Target = Document;
1453
1454 fn deref(&self) -> &Document {
1455 &self.doc
1456 }
1457}
1458
1459impl std::ops::DerefMut for DocumentView<'_> {
1460 fn deref_mut(&mut self) -> &mut Document {
1461 &mut self.doc
1462 }
1463}
1464
1465impl Drop for Document {
1466 fn drop(&mut self) {
1467 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
1468 }
1469}
1470
1471#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1481pub struct MarkdownExtensions {
1482 pub directives: bool,
1484 pub math: bool,
1486 pub html_elements: bool,
1491 pub highlight: bool,
1494 pub highlight_colors: bool,
1500}
1501
1502impl MarkdownExtensions {
1503 fn to_flags(self) -> u32 {
1504 let mut flags = 0;
1505 if self.directives {
1506 flags |= ffi::TWIG_MD_DIRECTIVES;
1507 }
1508 if self.math {
1509 flags |= ffi::TWIG_MD_MATH;
1510 }
1511 if self.html_elements {
1512 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
1513 }
1514 if self.highlight {
1515 flags |= ffi::TWIG_MD_HIGHLIGHT;
1516 }
1517 if self.highlight_colors {
1518 flags |= ffi::TWIG_MD_HIGHLIGHT_COLORS;
1519 }
1520 flags
1521 }
1522}
1523
1524#[derive(Debug)]
1530pub struct Editor {
1531 raw: NonNull<ffi::TwigEditor>,
1532}
1533
1534impl Editor {
1535 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
1538 let mut raw = std::ptr::null_mut();
1539 let ffi_format: ffi::TwigFormat = format.into();
1540 let status = unsafe {
1541 ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw)
1542 };
1543 Error::from_status(status)?;
1544 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1545 Ok(Self { raw })
1546 }
1547
1548 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
1549 Self::new(input.as_bytes(), format)
1550 }
1551
1552 pub fn new_ext(
1566 input: &[u8],
1567 format: Format,
1568 extensions: MarkdownExtensions,
1569 ) -> Result<Self, Error> {
1570 let mut raw = std::ptr::null_mut();
1571 let ffi_format: ffi::TwigFormat = format.into();
1572 let status = unsafe {
1573 ffi::twig_editor_create_ext(
1574 input.as_ptr(),
1575 input.len(),
1576 ffi_format as i32,
1577 extensions.to_flags(),
1578 &mut raw,
1579 )
1580 };
1581 Error::from_status(status)?;
1582 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1583 Ok(Self { raw })
1584 }
1585
1586 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1588 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1589 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
1590 })
1591 }
1592
1593 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1596 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1597 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
1598 })
1599 }
1600
1601 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1603 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1604 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
1605 })
1606 }
1607
1608 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1610 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1611 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
1612 })
1613 }
1614
1615 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
1618 let status = unsafe {
1619 ffi::twig_editor_insert_child(
1620 self.raw.as_ptr(),
1621 locator.as_ptr(),
1622 locator.len(),
1623 index,
1624 text.as_ptr(),
1625 text.len(),
1626 )
1627 };
1628 Error::from_status(status)
1629 }
1630
1631 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
1634 let status =
1635 unsafe { ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1636 Error::from_status(status)
1637 }
1638
1639 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
1642 let status = unsafe {
1643 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
1644 };
1645 Error::from_status(status)
1646 }
1647
1648 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
1652 let status =
1653 unsafe { ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1654 Error::from_status(status)
1655 }
1656
1657 pub fn filter(
1662 &mut self,
1663 drop: &str,
1664 keep: Option<&str>,
1665 unwrap_kept: bool,
1666 ) -> Result<(), Error> {
1667 let (keep_ptr, keep_len) = match keep {
1668 Some(k) => (k.as_ptr(), k.len()),
1669 None => (std::ptr::null(), 0),
1670 };
1671 let status = unsafe {
1672 ffi::twig_editor_filter(
1673 self.raw.as_ptr(),
1674 drop.as_ptr(),
1675 drop.len(),
1676 keep_ptr,
1677 keep_len,
1678 unwrap_kept as i32,
1679 )
1680 };
1681 Error::from_status(status)
1682 }
1683
1684 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
1686 let raw = self.raw.as_ptr();
1687 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
1688 }
1689
1690 pub fn source_str(&mut self) -> Result<String, Error> {
1692 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
1693 }
1694
1695 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
1698 let raw = self.raw.as_ptr();
1699 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
1700 }
1701
1702 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1705 let raw = self.raw.as_ptr();
1706 collect_matches(|ptr, len| unsafe {
1707 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
1708 })
1709 }
1710
1711 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
1721 let mut change = ffi::TwigChange {
1722 old_span: ffi::TwigSpan { start: 0, end: 0 },
1723 new_span: ffi::TwigSpan { start: 0, end: 0 },
1724 };
1725 let status = unsafe {
1726 ffi::twig_editor_edit_range(
1727 self.raw.as_ptr(),
1728 start,
1729 end,
1730 text.as_ptr(),
1731 text.len(),
1732 &mut change,
1733 )
1734 };
1735 Error::from_status(status)?;
1736 Ok(Change::from_ffi(change))
1737 }
1738
1739 pub fn last_change(&mut self) -> Option<Change> {
1745 let mut change = ffi::TwigChange {
1746 old_span: ffi::TwigSpan { start: 0, end: 0 },
1747 new_span: ffi::TwigSpan { start: 0, end: 0 },
1748 };
1749 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
1750 match status.0 {
1751 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
1752 _ => None,
1753 }
1754 }
1755
1756 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
1761 let mut change = ffi::TwigChange {
1762 old_span: ffi::TwigSpan { start: 0, end: 0 },
1763 new_span: ffi::TwigSpan { start: 0, end: 0 },
1764 };
1765 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
1766 if status.0 == ffi::TwigStatus::NOT_FOUND {
1767 return Ok(None);
1768 }
1769 Error::from_status(status)?;
1770 Ok(Some(Change::from_ffi(change)))
1771 }
1772
1773 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
1777 let mut change = ffi::TwigChange {
1778 old_span: ffi::TwigSpan { start: 0, end: 0 },
1779 new_span: ffi::TwigSpan { start: 0, end: 0 },
1780 };
1781 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
1782 if status.0 == ffi::TwigStatus::NOT_FOUND {
1783 return Ok(None);
1784 }
1785 Error::from_status(status)?;
1786 Ok(Some(Change::from_ffi(change)))
1787 }
1788
1789 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
1794 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
1795 Error::from_status(status)
1796 }
1797
1798 pub fn revision(&mut self) -> u64 {
1804 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
1805 }
1806
1807 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
1828 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1829 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
1830 match status.0 {
1831 ffi::TwigStatus::OK => Some(span.start..span.end),
1832 _ => None,
1833 }
1834 }
1835
1836 pub fn clear_dirty(&mut self) {
1841 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
1842 }
1843
1844 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
1852 let status = unsafe {
1853 ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len())
1854 };
1855 Error::from_status(status)
1856 }
1857
1858 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
1863 let raw = self.raw.as_ptr();
1864 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
1865 }
1866
1867 pub fn document(&mut self) -> Result<DocumentView<'_>, Error> {
1876 let mut raw = std::ptr::null_mut();
1877 let status = unsafe { ffi::twig_editor_document(self.raw.as_ptr(), &mut raw) };
1878 Error::from_status(status)?;
1879 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1880 Ok(DocumentView {
1881 doc: Document { raw },
1882 _editor: PhantomData,
1883 })
1884 }
1885
1886 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1891 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1892 let mut len = 0usize;
1893 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
1894 Error::from_status(status)?;
1895 if len == 0 {
1896 return Ok(Vec::new());
1897 }
1898 if ptr.is_null() {
1899 return Err(Error::Internal);
1900 }
1901 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1902 raw.iter().map(flat_node_from_ffi).collect()
1903 }
1904
1905 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1912 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1913 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1914 let mut len = 0usize;
1915 let status =
1916 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
1917 Error::from_status(status)?;
1918 if len == 0 || ptr.is_null() {
1919 return Ok(Vec::new());
1920 }
1921 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1922 raw.iter().map(query_match_from_ffi).collect()
1923 }
1924
1925 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1933 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1934 let mut len = 0usize;
1935 let status =
1936 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
1937 Error::from_status(status)?;
1938 if len == 0 || ptr.is_null() {
1939 return Ok(Vec::new());
1940 }
1941 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1942 raw.iter().map(flat_node_from_ffi).collect()
1943 }
1944
1945 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1950 let mut m = ffi::TwigQueryMatch {
1951 node_id: 0,
1952 span: ffi::TwigSpan { start: 0, end: 0 },
1953 content_span: ffi::TwigSpan { start: 0, end: 0 },
1954 has_content_span: 0,
1955 kind: std::ptr::null(),
1956 };
1957 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
1958 match status.0 {
1959 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1960 ffi::TwigStatus::NOT_FOUND => Ok(None),
1961 _ => Err(Error::from_status(status).unwrap_err()),
1962 }
1963 }
1964
1965 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1969 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1970 let mut len = 0usize;
1971 let status =
1972 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
1973 match status.0 {
1974 ffi::TwigStatus::OK => {}
1975 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1976 _ => return Err(Error::from_status(status).unwrap_err()),
1977 }
1978 if len == 0 || ptr.is_null() {
1979 return Ok(Vec::new());
1980 }
1981 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1982 raw.iter().map(query_match_from_ffi).collect()
1983 }
1984
1985 pub fn wrap_range(
2012 &mut self,
2013 start: usize,
2014 end: usize,
2015 kind: InlineKind,
2016 ) -> Result<Change, Error> {
2017 self.change_op(|ed, out| unsafe {
2018 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
2019 })
2020 }
2021
2022 pub fn toggle_inline(
2032 &mut self,
2033 start: usize,
2034 end: usize,
2035 kind: InlineKind,
2036 ) -> Result<Change, Error> {
2037 self.change_op(|ed, out| unsafe {
2038 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
2039 })
2040 }
2041
2042 pub fn set_mark_color(
2080 &mut self,
2081 offset: usize,
2082 color: Option<MarkColor>,
2083 ) -> Result<Change, Error> {
2084 let name = color.map(MarkColor::as_str);
2085 let (ptr, len, has) = opt_str(name);
2086 self.change_op(|ed, out| unsafe {
2087 ffi::twig_editor_set_mark_color(ed, offset, ptr, len, has, out)
2088 })
2089 }
2090
2091 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
2114 let (block_kind, level) = kind.to_c();
2115 self.change_op(|ed, out| unsafe {
2116 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
2117 })
2118 }
2119
2120 pub fn toggle_block_container(
2143 &mut self,
2144 start: usize,
2145 end: usize,
2146 kind: BlockContainerKind,
2147 ) -> Result<Change, Error> {
2148 self.change_op(|ed, out| unsafe {
2149 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
2150 })
2151 }
2152
2153 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
2172 self.change_op(|ed, out| unsafe {
2173 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
2174 })?;
2175 Ok(())
2176 }
2177
2178 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
2187 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
2188 }
2189
2190 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
2193 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
2194 }
2195
2196 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
2198 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
2199 }
2200
2201 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
2203 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
2204 }
2205
2206 pub fn table_set_alignment(
2208 &mut self,
2209 offset: usize,
2210 alignment: Alignment,
2211 ) -> Result<(), Error> {
2212 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
2213 }
2214
2215 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
2217 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
2218 }
2219
2220 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
2222 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
2223 }
2224
2225 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
2226 self.change_op(|ed, out| unsafe { ffi::twig_editor_table_edit(ed, offset, op, arg, out) })?;
2227 Ok(())
2228 }
2229
2230 pub fn insert_link(
2275 &mut self,
2276 start: usize,
2277 end: usize,
2278 destination: &str,
2279 ) -> Result<Change, Error> {
2280 self.change_op(|ed, out| unsafe {
2281 ffi::twig_editor_insert_link(
2282 ed,
2283 start,
2284 end,
2285 destination.as_ptr(),
2286 destination.len(),
2287 out,
2288 )
2289 })
2290 }
2291
2292 pub fn insert_image(
2313 &mut self,
2314 start: usize,
2315 end: usize,
2316 destination: &str,
2317 ) -> Result<Change, Error> {
2318 self.change_op(|ed, out| unsafe {
2319 ffi::twig_editor_insert_image(
2320 ed,
2321 start,
2322 end,
2323 destination.as_ptr(),
2324 destination.len(),
2325 out,
2326 )
2327 })
2328 }
2329
2330 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
2355 self.change_op(|ed, out| unsafe {
2356 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
2357 })
2358 }
2359
2360 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
2374 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
2375 }
2376
2377 pub fn insert_thematic_break(&mut self, offset: usize) -> Result<Change, Error> {
2396 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_thematic_break(ed, offset, out) })
2397 }
2398
2399 pub fn split_block(&mut self, offset: usize) -> Result<Change, Error> {
2447 self.change_op(|ed, out| unsafe { ffi::twig_editor_split_block(ed, offset, out) })
2448 }
2449
2450 pub fn toggle_code_block(
2482 &mut self,
2483 start: usize,
2484 end: usize,
2485 language: Option<&str>,
2486 ) -> Result<Change, Error> {
2487 let (ptr, len, has) = opt_str(language);
2488 self.change_op(|ed, out| unsafe {
2489 ffi::twig_editor_toggle_code_block(ed, start, end, ptr, len, has, out)
2490 })
2491 }
2492
2493 pub fn set_code_language(
2503 &mut self,
2504 offset: usize,
2505 language: Option<&str>,
2506 ) -> Result<Change, Error> {
2507 let (ptr, len, has) = opt_str(language);
2508 self.change_op(|ed, out| unsafe {
2509 ffi::twig_editor_set_code_language(ed, offset, ptr, len, has, out)
2510 })
2511 }
2512
2513 pub fn toggle_task_item(&mut self, offset: usize) -> Result<Change, Error> {
2524 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_item(ed, offset, out) })
2525 }
2526
2527 pub fn set_task_checked(&mut self, offset: usize, checked: bool) -> Result<(), Error> {
2541 self.change_op(|ed, out| unsafe {
2542 ffi::twig_editor_set_task_checked(ed, offset, checked as c_int, out)
2543 })?;
2544 Ok(())
2545 }
2546
2547 pub fn toggle_task_checked(&mut self, offset: usize) -> Result<Change, Error> {
2552 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_checked(ed, offset, out) })
2553 }
2554
2555 pub fn insert_footnote(&mut self, offset: usize, label: &str) -> Result<Change, Error> {
2574 self.change_op(|ed, out| unsafe {
2575 ffi::twig_editor_insert_footnote(ed, offset, label.as_ptr(), label.len(), out)
2576 })
2577 }
2578
2579 fn change_op(
2582 &mut self,
2583 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
2584 ) -> Result<Change, Error> {
2585 let mut change = ffi::TwigChange {
2586 old_span: ffi::TwigSpan { start: 0, end: 0 },
2587 new_span: ffi::TwigSpan { start: 0, end: 0 },
2588 };
2589 let status = op(self.raw.as_ptr(), &mut change);
2590 Error::from_status(status)?;
2591 Ok(Change::from_ffi(change))
2592 }
2593
2594 fn apply(
2596 &mut self,
2597 locator: &str,
2598 text: &str,
2599 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
2600 ) -> Result<(), Error> {
2601 let status = op(
2602 self.raw.as_ptr(),
2603 locator.as_ptr(),
2604 locator.len(),
2605 text.as_ptr(),
2606 text.len(),
2607 );
2608 Error::from_status(status)
2609 }
2610}
2611
2612impl Drop for Editor {
2613 fn drop(&mut self) {
2614 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
2615 }
2616}
2617
2618fn collect_bytes(
2623 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
2624) -> Result<Vec<u8>, Error> {
2625 let mut ptr = std::ptr::null();
2626 let mut len = 0usize;
2627 let status = call(&mut ptr, &mut len);
2628 Error::from_status(status)?;
2629 if len == 0 {
2630 return Ok(Vec::new());
2631 }
2632 if ptr.is_null() {
2633 return Err(Error::Internal);
2634 }
2635 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2636 Ok(bytes.to_vec())
2637}
2638
2639fn collect_matches(
2642 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
2643) -> Result<Vec<QueryMatch>, Error> {
2644 let mut ptr = std::ptr::null();
2645 let mut len = 0usize;
2646 let status = call(&mut ptr, &mut len);
2647 Error::from_status(status)?;
2648 if len == 0 {
2649 return Ok(Vec::new());
2650 }
2651 if ptr.is_null() {
2652 return Err(Error::Internal);
2653 }
2654 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
2655 matches.iter().map(query_match_from_ffi).collect()
2656}
2657
2658fn collect_flat_nodes(
2661 call: impl FnOnce(*mut *const ffi::TwigFlatNode, *mut usize) -> ffi::TwigStatus,
2662) -> Result<Vec<FlatNode>, Error> {
2663 let mut ptr = std::ptr::null();
2664 let mut len = 0usize;
2665 let status = call(&mut ptr, &mut len);
2666 Error::from_status(status)?;
2667 if len == 0 {
2668 return Ok(Vec::new());
2669 }
2670 if ptr.is_null() {
2671 return Err(Error::Internal);
2672 }
2673 let nodes = unsafe { std::slice::from_raw_parts(ptr, len) };
2674 nodes.iter().map(flat_node_from_ffi).collect()
2675}
2676
2677fn empty_ffi_match() -> ffi::TwigQueryMatch {
2679 ffi::TwigQueryMatch {
2680 node_id: 0,
2681 span: ffi::TwigSpan { start: 0, end: 0 },
2682 content_span: ffi::TwigSpan { start: 0, end: 0 },
2683 has_content_span: 0,
2684 kind: std::ptr::null(),
2685 }
2686}
2687
2688fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
2691 Ok(QueryMatch {
2692 node_id: m.node_id,
2693 span: m.span.start..m.span.end,
2694 content_span: if m.has_content_span != 0 {
2695 Some(m.content_span.start..m.content_span.end)
2696 } else {
2697 None
2698 },
2699 kind: Kind::from(borrowed_cstr(m.kind)?.as_str()),
2700 })
2701}
2702
2703fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
2705 let node_id = |v: u32| {
2706 if v == ffi::TWIG_NO_NODE {
2707 None
2708 } else {
2709 Some(NodeId(v))
2710 }
2711 };
2712 Ok(FlatNode {
2713 id: NodeId(n.id),
2714 parent: node_id(n.parent),
2715 first_child: node_id(n.first_child),
2716 next_sibling: node_id(n.next_sibling),
2717 span: n.span.start..n.span.end,
2718 content_span: if n.has_content_span != 0 {
2719 Some(n.content_span.start..n.content_span.end)
2720 } else {
2721 None
2722 },
2723 level: if n.level != 0 { Some(n.level) } else { None },
2724 kind: Kind::from(borrowed_cstr(n.kind)?.as_str()),
2725 text: borrowed_bytes(n.text_ptr, n.text_len),
2726 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
2727 head: match n.head {
2728 ffi::TWIG_HEAD_NONE => None,
2729 v => Some(v != 0),
2730 },
2731 alignment: Alignment::from_c(n.alignment),
2732 name: borrowed_bytes(n.name_ptr, n.name_len),
2733 directive_form: DirectiveForm::from_c(n.directive_form),
2734 origin: ContainerOrigin::from_c(n.container_origin),
2735 marker_span: if n.has_marker_span != 0 {
2736 Some(n.marker_span.start..n.marker_span.end)
2737 } else {
2738 None
2739 },
2740 checked: match n.checked {
2741 ffi::TWIG_TASK_CHECKED_NONE => None,
2742 v => Some(v != 0),
2743 },
2744 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
2745 })
2746}
2747
2748fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
2752 if ptr.is_null() || len == 0 {
2753 return Vec::new();
2754 }
2755 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
2756 kvs.iter()
2757 .map(|kv| {
2758 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
2759 (key, borrowed_bytes(kv.value, kv.value_len))
2760 })
2761 .collect()
2762}
2763
2764fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
2766 if ptr.is_null() {
2767 return Err(Error::Internal);
2768 }
2769 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
2770 .to_str()
2771 .map_err(|_| Error::Internal)?
2772 .to_owned())
2773}
2774
2775fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
2779 if ptr.is_null() {
2780 return None;
2781 }
2782 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2783 Some(String::from_utf8_lossy(bytes).into_owned())
2784}
2785
2786#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
2790pub struct NodeId(pub u32);
2791
2792#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2795pub enum VoidKind {
2796 Doc,
2797 Para,
2798 ThematicBreak,
2799 Section,
2800 Div,
2801 BlockQuote,
2802 DefinitionList,
2803 Table,
2804 ListItem,
2805 DefinitionListItem,
2806 Term,
2807 Definition,
2808 Caption,
2809 SoftBreak,
2810 HardBreak,
2811 NonBreakingSpace,
2812 Emph,
2813 Strong,
2814 Span,
2815 Mark,
2816 Superscript,
2817 Subscript,
2818 Insert,
2819 Delete,
2820 DoubleQuoted,
2821 SingleQuoted,
2822}
2823
2824impl VoidKind {
2825 fn to_c(self) -> c_int {
2826 match self {
2828 VoidKind::Doc => 0,
2829 VoidKind::Para => 1,
2830 VoidKind::ThematicBreak => 3,
2831 VoidKind::Section => 4,
2832 VoidKind::Div => 5,
2833 VoidKind::BlockQuote => 9,
2834 VoidKind::DefinitionList => 13,
2835 VoidKind::Table => 14,
2836 VoidKind::ListItem => 15,
2837 VoidKind::DefinitionListItem => 17,
2838 VoidKind::Term => 18,
2839 VoidKind::Definition => 19,
2840 VoidKind::Caption => 22,
2841 VoidKind::SoftBreak => 26,
2842 VoidKind::HardBreak => 27,
2843 VoidKind::NonBreakingSpace => 28,
2844 VoidKind::Emph => 38,
2845 VoidKind::Strong => 39,
2846 VoidKind::Span => 42,
2847 VoidKind::Mark => 43,
2848 VoidKind::Superscript => 44,
2849 VoidKind::Subscript => 45,
2850 VoidKind::Insert => 46,
2851 VoidKind::Delete => 47,
2852 VoidKind::DoubleQuoted => 48,
2853 VoidKind::SingleQuoted => 49,
2854 }
2855 }
2856}
2857
2858#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2860pub enum TextKind {
2861 Str,
2862 Symb,
2863 Verbatim,
2864 InlineMath,
2865 DisplayMath,
2866 Url,
2867 Email,
2868 FootnoteReference,
2869 CitationReference,
2872 SubstitutionReference,
2874 Comment,
2875 Doctype,
2876 Cdata,
2877}
2878
2879impl TextKind {
2880 fn to_c(self) -> c_int {
2881 match self {
2882 TextKind::Str => 25,
2883 TextKind::Symb => 29,
2884 TextKind::Verbatim => 30,
2885 TextKind::InlineMath => 32,
2886 TextKind::DisplayMath => 33,
2887 TextKind::Url => 34,
2888 TextKind::Email => 35,
2889 TextKind::FootnoteReference => 36,
2890 TextKind::CitationReference => 58,
2891 TextKind::SubstitutionReference => 59,
2892 TextKind::Comment => 52,
2893 TextKind::Doctype => 53,
2894 TextKind::Cdata => 55,
2895 }
2896 }
2897}
2898
2899#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2901pub enum BulletStyle {
2902 Dash,
2903 Plus,
2904 Star,
2905}
2906
2907impl BulletStyle {
2908 fn to_c(self) -> c_int {
2909 match self {
2910 BulletStyle::Dash => 0,
2911 BulletStyle::Plus => 1,
2912 BulletStyle::Star => 2,
2913 }
2914 }
2915}
2916
2917#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2919pub enum OrderedNumbering {
2920 Decimal,
2921 LowerAlpha,
2922 UpperAlpha,
2923 LowerRoman,
2924 UpperRoman,
2925}
2926
2927impl OrderedNumbering {
2928 fn to_c(self) -> c_int {
2929 match self {
2930 OrderedNumbering::Decimal => 0,
2931 OrderedNumbering::LowerAlpha => 1,
2932 OrderedNumbering::UpperAlpha => 2,
2933 OrderedNumbering::LowerRoman => 3,
2934 OrderedNumbering::UpperRoman => 4,
2935 }
2936 }
2937}
2938
2939#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2941pub enum OrderedDelim {
2942 Period,
2943 ParenAfter,
2944 ParenBoth,
2945}
2946
2947impl OrderedDelim {
2948 fn to_c(self) -> c_int {
2949 match self {
2950 OrderedDelim::Period => 0,
2951 OrderedDelim::ParenAfter => 1,
2952 OrderedDelim::ParenBoth => 2,
2953 }
2954 }
2955}
2956
2957#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2960pub enum Alignment {
2961 Default,
2962 Left,
2963 Right,
2964 Center,
2965}
2966
2967impl Alignment {
2968 fn to_c(self) -> c_int {
2969 match self {
2970 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
2971 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
2972 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
2973 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
2974 }
2975 }
2976
2977 fn from_c(v: c_int) -> Option<Self> {
2980 match v {
2981 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
2982 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
2983 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
2984 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
2985 _ => None,
2986 }
2987 }
2988}
2989
2990#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2992pub enum SmartPunctuation {
2993 LeftSingleQuote,
2994 RightSingleQuote,
2995 LeftDoubleQuote,
2996 RightDoubleQuote,
2997 Ellipses,
2998 EmDash,
2999 EnDash,
3000}
3001
3002impl SmartPunctuation {
3003 fn to_c(self) -> c_int {
3004 match self {
3005 SmartPunctuation::LeftSingleQuote => 0,
3006 SmartPunctuation::RightSingleQuote => 1,
3007 SmartPunctuation::LeftDoubleQuote => 2,
3008 SmartPunctuation::RightDoubleQuote => 3,
3009 SmartPunctuation::Ellipses => 4,
3010 SmartPunctuation::EmDash => 5,
3011 SmartPunctuation::EnDash => 6,
3012 }
3013 }
3014}
3015
3016#[derive(Clone, Debug, Eq, PartialEq)]
3031pub struct Warning {
3032 pub fidelity: Fidelity,
3033 pub path: String,
3040 pub kind: Kind,
3043}
3044
3045#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3047#[non_exhaustive]
3048pub enum Fidelity {
3049 Degraded,
3052 Dropped,
3054}
3055
3056impl Fidelity {
3057 fn from_c(v: c_int) -> Self {
3061 match v {
3062 ffi::TWIG_FIDELITY_DROPPED => Fidelity::Dropped,
3063 _ => Fidelity::Degraded,
3064 }
3065 }
3066}
3067
3068#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3069#[non_exhaustive]
3070pub enum ContainerOrigin {
3071 Element,
3073 Directive,
3077}
3078
3079impl ContainerOrigin {
3080 fn from_c(v: c_int) -> Option<Self> {
3083 match v {
3084 ffi::TWIG_CONTAINER_ORIGIN_ELEMENT => Some(ContainerOrigin::Element),
3085 ffi::TWIG_CONTAINER_ORIGIN_DIRECTIVE => Some(ContainerOrigin::Directive),
3086 _ => None,
3087 }
3088 }
3089}
3090
3091#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3093pub enum DirectiveForm {
3094 Text,
3095 Leaf,
3096 Container,
3097}
3098
3099impl DirectiveForm {
3100 fn to_c(self) -> c_int {
3101 match self {
3102 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
3103 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
3104 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
3105 }
3106 }
3107
3108 fn from_c(v: c_int) -> Option<Self> {
3112 match v {
3113 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
3114 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
3115 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
3116 _ => None,
3117 }
3118 }
3119}
3120
3121fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
3125 match s {
3126 Some(x) => (x.as_ptr(), x.len(), 1),
3127 None => (std::ptr::null(), 0, 0),
3128 }
3129}
3130
3131#[derive(Debug)]
3138pub struct Builder {
3139 raw: NonNull<ffi::TwigBuilder>,
3140}
3141
3142impl Builder {
3143 pub fn new() -> Result<Self, Error> {
3145 let mut raw = std::ptr::null_mut();
3146 let status = unsafe { ffi::twig_builder_create(&mut raw) };
3147 Error::from_status(status)?;
3148 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
3149 Ok(Self { raw })
3150 }
3151
3152 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
3155 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
3156 }
3157
3158 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
3160 self.emit(|b, out| unsafe {
3161 ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out)
3162 })
3163 }
3164
3165 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
3167 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
3168 }
3169
3170 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
3172 let (lp, ll, has) = opt_str(lang);
3173 self.emit(|b, out| unsafe {
3174 ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out)
3175 })
3176 }
3177
3178 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
3180 self.emit(|b, out| unsafe {
3181 ffi::twig_builder_add_raw_block(
3182 b,
3183 format.as_ptr(),
3184 format.len(),
3185 text.as_ptr(),
3186 text.len(),
3187 out,
3188 )
3189 })
3190 }
3191
3192 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
3194 self.emit(|b, out| unsafe {
3195 ffi::twig_builder_add_metadata(
3196 b,
3197 lang.as_ptr(),
3198 lang.len(),
3199 text.as_ptr(),
3200 text.len(),
3201 out,
3202 )
3203 })
3204 }
3205
3206 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
3208 self.emit(|b, out| unsafe {
3209 ffi::twig_builder_add_raw_inline(
3210 b,
3211 format.as_ptr(),
3212 format.len(),
3213 text.as_ptr(),
3214 text.len(),
3215 out,
3216 )
3217 })
3218 }
3219
3220 pub fn add_smart_punctuation(
3225 &mut self,
3226 kind: SmartPunctuation,
3227 text: &str,
3228 ) -> Result<NodeId, Error> {
3229 self.emit(|b, out| unsafe {
3230 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
3231 })
3232 }
3233
3234 pub fn add_link(
3237 &mut self,
3238 destination: Option<&str>,
3239 reference: Option<&str>,
3240 ) -> Result<NodeId, Error> {
3241 let (dp, dl, hd) = opt_str(destination);
3242 let (rp, rl, hr) = opt_str(reference);
3243 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
3244 }
3245
3246 pub fn add_image(
3248 &mut self,
3249 destination: Option<&str>,
3250 reference: Option<&str>,
3251 ) -> Result<NodeId, Error> {
3252 let (dp, dl, hd) = opt_str(destination);
3253 let (rp, rl, hr) = opt_str(reference);
3254 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
3255 }
3256
3257 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
3259 self.emit(|b, out| unsafe {
3260 ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out)
3261 })
3262 }
3263
3264 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
3266 self.emit(|b, out| unsafe {
3267 ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out)
3268 })
3269 }
3270
3271 pub fn add_processing_instruction(
3273 &mut self,
3274 target: &str,
3275 data: &str,
3276 ) -> Result<NodeId, Error> {
3277 self.emit(|b, out| unsafe {
3278 ffi::twig_builder_add_processing_instruction(
3279 b,
3280 target.as_ptr(),
3281 target.len(),
3282 data.as_ptr(),
3283 data.len(),
3284 out,
3285 )
3286 })
3287 }
3288
3289 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
3291 self.emit(|b, out| unsafe {
3292 ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out)
3293 })
3294 }
3295
3296 pub fn add_citation(&mut self, label: &str) -> Result<NodeId, Error> {
3301 self.emit(|b, out| unsafe {
3302 ffi::twig_builder_add_citation(b, label.as_ptr(), label.len(), out)
3303 })
3304 }
3305
3306 pub fn add_substitution(&mut self, label: &str) -> Result<NodeId, Error> {
3310 self.emit(|b, out| unsafe {
3311 ffi::twig_builder_add_substitution(b, label.as_ptr(), label.len(), out)
3312 })
3313 }
3314
3315 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
3317 self.emit(|b, out| unsafe {
3318 ffi::twig_builder_add_reference(
3319 b,
3320 label.as_ptr(),
3321 label.len(),
3322 destination.as_ptr(),
3323 destination.len(),
3324 out,
3325 )
3326 })
3327 }
3328
3329 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
3331 self.emit(|b, out| unsafe {
3332 ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out)
3333 })
3334 }
3335
3336 pub fn add_ordered_list(
3338 &mut self,
3339 numbering: OrderedNumbering,
3340 delim: OrderedDelim,
3341 tight: bool,
3342 start: Option<u32>,
3343 ) -> Result<NodeId, Error> {
3344 let (start_val, has_start) = match start {
3345 Some(s) => (s, 1),
3346 None => (0, 0),
3347 };
3348 self.emit(|b, out| unsafe {
3349 ffi::twig_builder_add_ordered_list(
3350 b,
3351 numbering.to_c(),
3352 delim.to_c(),
3353 tight as c_int,
3354 start_val,
3355 has_start,
3356 out,
3357 )
3358 })
3359 }
3360
3361 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
3363 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
3364 }
3365
3366 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
3368 self.emit(|b, out| unsafe {
3369 ffi::twig_builder_add_task_list_item(b, checked as c_int, out)
3370 })
3371 }
3372
3373 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
3375 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
3376 }
3377
3378 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
3380 self.emit(|b, out| unsafe {
3381 ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out)
3382 })
3383 }
3384
3385 pub fn add_cell_spanning(
3390 &mut self,
3391 head: bool,
3392 alignment: Alignment,
3393 colspan: u32,
3394 rowspan: u32,
3395 ) -> Result<NodeId, Error> {
3396 self.emit(|b, out| unsafe {
3397 ffi::twig_builder_add_cell_spanning(
3398 b,
3399 head as c_int,
3400 alignment.to_c(),
3401 colspan,
3402 rowspan,
3403 out,
3404 )
3405 })
3406 }
3407
3408 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
3411 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
3412 let status = unsafe {
3413 ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len())
3414 };
3415 Error::from_status(status)
3416 }
3417
3418 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
3422 let kvs: Vec<ffi::TwigKeyVal> = attrs
3423 .iter()
3424 .map(|(k, v)| ffi::TwigKeyVal {
3425 key: k.as_ptr(),
3426 key_len: k.len(),
3427 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
3428 value_len: v.map_or(0, |s| s.len()),
3429 })
3430 .collect();
3431 let status = unsafe {
3432 ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len())
3433 };
3434 Error::from_status(status)
3435 }
3436
3437 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3440 let raw = self.raw.as_ptr();
3441 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
3442 }
3443
3444 pub fn serialize_to(&mut self, root: NodeId, target: Target) -> Result<Vec<u8>, Error> {
3451 let raw = self.raw.as_ptr();
3452 let ffi_target: ffi::TwigFormat = target.into();
3453 collect_bytes(|ptr, len| unsafe {
3454 ffi::twig_builder_serialize(raw, root.0, ffi_target as i32, ptr, len)
3455 })
3456 }
3457
3458 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
3463 self.serialize_to(root, format.into())
3464 }
3465
3466 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3468 let raw = self.raw.as_ptr();
3469 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
3470 }
3471
3472 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
3475 let raw = self.raw.as_ptr();
3476 collect_matches(|ptr, len| unsafe {
3477 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
3478 })
3479 }
3480
3481 fn emit(
3484 &mut self,
3485 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
3486 ) -> Result<NodeId, Error> {
3487 let mut id: u32 = 0;
3488 let status = call(self.raw.as_ptr(), &mut id);
3489 Error::from_status(status)?;
3490 Ok(NodeId(id))
3491 }
3492}
3493
3494impl Drop for Builder {
3495 fn drop(&mut self) {
3496 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
3497 }
3498}
3499
3500#[cfg(test)]
3501mod tests {
3502 use super::*;
3503
3504 #[test]
3505 fn abi_version_matches() {
3506 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
3510 }
3511
3512 #[test]
3513 fn parses_and_renders_markdown_html() {
3514 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3515 let html = doc.render_html().expect("render html");
3516 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
3517 }
3518
3519 #[test]
3520 fn parses_html_input() {
3521 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
3522 let html = doc.render_html().expect("render html");
3523 assert!(String::from_utf8_lossy(&html).contains("hi"));
3524 }
3525
3526 #[test]
3527 fn parses_renders_and_writes_asciidoc() {
3528 let mut doc = Document::parse_str("= Title\n\nsome *bold* text\n", Format::Asciidoc)
3529 .expect("parse asciidoc");
3530 let html = String::from_utf8_lossy(&doc.render_html().expect("render html")).into_owned();
3531 assert!(html.contains("<h1>Title</h1>"), "got {html:?}");
3532 assert!(html.contains("<strong>bold</strong>"), "got {html:?}");
3533
3534 let back = doc.serialize_to(Target::Asciidoc).expect("serialize asciidoc");
3536 assert_eq!(String::from_utf8_lossy(&back), "= Title\n\nsome *bold* text\n");
3537 let mut md = Document::parse_str("# Title\n\nsome **bold** text\n", Format::Markdown)
3538 .expect("parse markdown");
3539 let converted = md.serialize_to(Target::Asciidoc).expect("convert to asciidoc");
3540 assert_eq!(String::from_utf8_lossy(&converted), "= Title\n\nsome *bold* text\n");
3541 assert_eq!(Target::from(Format::Asciidoc), Target::Asciidoc);
3542 assert_eq!(Target::Asciidoc.as_format(), Some(Format::Asciidoc));
3543 }
3544
3545 #[test]
3546 fn markdown_dialects_are_formats_over_one_parser() {
3547 let src = "a ~~b~~ c\n\n| x |\n| - |\n| $m$ |\n";
3551 let count = |doc: &mut Document, sel: &str| doc.query(sel).expect("query").len();
3552 for (format, ext, delete, table, math) in [
3553 (Format::Commonmark, MarkdownExtensions::default(), 0, 0, 0),
3554 (Format::Markdown, MarkdownExtensions::default(), 1, 1, 0),
3555 (Format::Gfm, MarkdownExtensions::default(), 1, 1, 0),
3556 (Format::Gfm, MarkdownExtensions { math: true, ..Default::default() }, 1, 1, 1),
3557 ] {
3558 let mut doc = Document::parse_str_with(src, format, ext).expect("parse");
3559 assert_eq!(count(&mut doc, "delete"), delete, "{format:?} {ext:?}");
3560 assert_eq!(count(&mut doc, "table"), table, "{format:?} {ext:?}");
3561 assert_eq!(count(&mut doc, "inline_math"), math, "{format:?} {ext:?}");
3562 }
3563
3564 assert_eq!(Format::Gfm.dialect_of(), Some(Format::Markdown));
3567 assert_eq!(Format::Commonmark.dialect_of(), Some(Format::Markdown));
3568 assert_eq!(Format::Markdown.dialect_of(), None);
3569 assert_eq!(Target::from(Format::Gfm), Target::Markdown);
3570 let mut gfm = Document::parse_str("* a ~~b~~\n", Format::Gfm).expect("parse gfm");
3571 let back = gfm.serialize(Format::Gfm).expect("serialize");
3572 assert_eq!(String::from_utf8_lossy(&back), "* a ~~b~~\n");
3573
3574 let mut table = Document::parse_str("| a |\n| :-: |\n| 1 |\n", Format::Gfm).expect("parse");
3576 let html = String::from_utf8_lossy(&table.render_html().expect("render")).into_owned();
3577 assert!(html.contains("align=\"center\""), "got {html:?}");
3578
3579 assert!(!Format::Commonmark.supports(Gesture::ToggleInline(InlineKind::Delete)));
3581 assert!(Format::Gfm.supports(Gesture::ToggleInline(InlineKind::Delete)));
3582 }
3583
3584 #[test]
3585 fn serialize_round_trips_and_cross_converts() {
3586 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3587
3588 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
3589 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
3590
3591 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
3593 }
3594
3595 #[test]
3596 fn serialize_markdown_to_djot() {
3597 let mut doc =
3598 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3599 let djot = doc.serialize(Format::Djot).expect("serialize djot");
3600 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3601 }
3602
3603 #[test]
3604 fn serialize_to_takes_the_output_axis() {
3605 let mut doc =
3606 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3607
3608 let djot = doc.serialize_to(Target::Djot).expect("serialize djot");
3609 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3610
3611 assert_eq!(doc.serialize_to(Target::Xml), Err(Error::UnsupportedFormat));
3614 }
3615
3616 #[test]
3617 fn serialize_and_serialize_to_agree() {
3618 let mut a = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3621 let mut b = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3622 for format in [Format::Markdown, Format::Djot, Format::Html] {
3623 assert_eq!(a.serialize(format), b.serialize_to(Target::from(format)));
3624 }
3625 }
3626
3627 #[test]
3628 fn every_format_is_a_target_that_names_it_back() {
3629 for format in [Format::Djot, Format::Markdown, Format::Xml, Format::Html] {
3632 assert_eq!(Target::from(format).as_format(), Some(format));
3633 }
3634 }
3635
3636 #[test]
3637 fn ast_json_dumps_the_tree() {
3638 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
3639 let json = doc.ast_json().expect("ast json");
3640 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
3641 }
3642
3643 #[test]
3644 fn query_finds_nodes_by_selector() {
3645 let source = "# One\n\n## Two\n";
3646 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3647 let matches = doc.query("heading").expect("query");
3648
3649 assert_eq!(matches.len(), 2);
3650 for m in &matches {
3651 assert_eq!(m.kind, Kind::Heading);
3652 assert!(m.span.start < m.span.end);
3653 }
3654 }
3655
3656 #[test]
3657 fn query_recovers_code_spans() {
3658 let source = "prose `code` more prose\n";
3659 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3660 let matches = doc.query("verbatim").expect("query");
3661
3662 assert_eq!(matches.len(), 1);
3663 assert_eq!(&source[matches[0].span.clone()], "`code`");
3664 }
3665
3666 #[test]
3667 fn document_span_accessors_read_by_node_id() {
3668 let source = "# hi\n\ntext\n";
3669 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3670 let heading = doc.query("heading").expect("query").pop().expect("heading");
3671
3672 assert_eq!(
3673 doc.span(NodeId(heading.node_id)).expect("span"),
3674 heading.span
3675 );
3676 assert_eq!(
3677 doc.content_span(NodeId(heading.node_id))
3678 .expect("content span"),
3679 heading.content_span
3680 );
3681 assert_eq!(doc.span(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3682 }
3683
3684 #[test]
3685 fn document_walks_its_tree_without_an_editor() {
3686 let source = "# hi\n\ntext\n";
3687 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3688
3689 let nodes = doc.nodes().expect("nodes");
3690 assert!(nodes.len() >= 3);
3691 for (i, n) in nodes.iter().enumerate() {
3692 assert_eq!(n.id, NodeId(i as u32));
3693 }
3694
3695 let kids = doc.children(None).expect("children");
3696 assert_eq!(kids.len(), 2);
3697 assert_eq!(kids[0].kind, Kind::Heading);
3698
3699 let sub = doc.subtree(NodeId(kids[0].node_id)).expect("subtree");
3700 assert_eq!(sub[0].id, NodeId(0));
3701 assert_eq!(sub[0].parent, None);
3702 assert_eq!(sub[0].span, kids[0].span);
3703
3704 let hit = doc.node_at(2).expect("node_at").expect("a node at 2");
3705 let chain = doc.ancestors_at(2).expect("ancestors");
3706 assert_eq!(chain.last().expect("deepest").node_id, hit.node_id);
3707 assert_eq!(chain[0].kind, Kind::Doc);
3708
3709 assert_eq!(doc.subtree(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3710 }
3711
3712 #[test]
3713 fn editor_document_view_reads_the_live_tree() {
3714 let mut ed = Editor::new_str("# one\n\ntwo\n", Format::Markdown).expect("editor");
3715
3716 {
3717 let mut view = ed.document().expect("view");
3718 let kids = view.children(None).expect("children");
3719 assert_eq!(kids.len(), 2);
3720 assert_eq!(kids[0].kind, Kind::Heading);
3721 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..5);
3722 assert_eq!(view.render_html(), Err(Error::UnsupportedFormat));
3724 assert_eq!(
3725 view.serialize(Format::Markdown),
3726 Err(Error::UnsupportedFormat)
3727 );
3728 }
3729
3730 ed.replace("0", "# one and a half").expect("replace");
3731 let mut view = ed.document().expect("view");
3732 let kids = view.children(None).expect("children");
3733 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..16);
3734 }
3735
3736 #[test]
3737 fn query_rejects_a_malformed_selector() {
3738 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
3739 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
3740 }
3741
3742 #[test]
3743 fn editor_edits_by_index_path() {
3744 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
3745 ed.replace_content("0.0", "bye").expect("replace_content");
3746 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
3747 }
3748
3749 #[test]
3750 fn flat_nodes_expose_element_name_and_attrs() {
3751 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
3755 let mut ed = Editor::new_ext(
3756 src.as_bytes(),
3757 Format::Markdown,
3758 MarkdownExtensions {
3759 html_elements: true,
3760 ..Default::default()
3761 },
3762 )
3763 .expect("editor");
3764 let nodes = ed.nodes().expect("nodes");
3765
3766 let source = nodes
3767 .iter()
3768 .find(|n| n.name.as_deref() == Some("source"))
3769 .expect("a <source> element node");
3770 assert_eq!(
3771 source.attrs,
3772 vec![
3773 (
3774 "media".to_string(),
3775 Some("(prefers-color-scheme: dark)".to_string())
3776 ),
3777 ("srcset".to_string(), Some("d.svg".to_string())),
3778 ]
3779 );
3780
3781 let img = nodes
3784 .iter()
3785 .find(|n| n.kind == Kind::Image)
3786 .expect("an image node");
3787 assert!(img.name.is_none());
3788 assert_eq!(img.destination.as_deref(), Some("l.svg"));
3789
3790 let picture_kids_str = nodes.iter().find(|n| n.kind == Kind::Str);
3792 if let Some(s) = picture_kids_str {
3793 assert!(s.name.is_none() && s.attrs.is_empty());
3794 }
3795 }
3796
3797 #[test]
3798 fn definitions_finds_what_a_walk_from_the_root_cannot() {
3799 let mut doc = Document::parse_str(
3803 "text[^1] [x][a]\n\n[^1]: note\n\n[a]: /u\n",
3804 Format::Markdown,
3805 )
3806 .expect("parse markdown");
3807
3808 let defs = doc.definitions().expect("definitions");
3809 let mut kinds: Vec<Kind> = defs.iter().map(|m| m.kind.clone()).collect();
3810 kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3811 assert_eq!(kinds, vec![Kind::Footnote, Kind::Reference]);
3812
3813 for d in &defs {
3817 let want = match d.kind {
3818 Kind::Footnote => 17..27,
3819 Kind::Reference => 29..36,
3820 _ => unreachable!(),
3821 };
3822 assert_eq!(d.span, want, "{} stands on its own bytes", d.kind);
3823 }
3824
3825 let all = doc.nodes().expect("nodes");
3828 let root = all
3829 .iter()
3830 .find(|n| n.kind == Kind::Doc)
3831 .expect("a doc root");
3832 let mut reachable = vec![root.id];
3833 let mut i = 0;
3834 while i < reachable.len() {
3835 let n = &all[reachable[i].0 as usize];
3836 let mut c = n.first_child;
3837 while let Some(cid) = c {
3838 reachable.push(cid);
3839 c = all[cid.0 as usize].next_sibling;
3840 }
3841 i += 1;
3842 }
3843 for d in &defs {
3844 assert!(
3845 !reachable.contains(&NodeId(d.node_id)),
3846 "{} should be unreachable from the root",
3847 d.kind
3848 );
3849 }
3850
3851 let mut plain = Document::parse_str("just text\n", Format::Markdown).expect("parse");
3853 assert_eq!(plain.definitions().expect("definitions"), Vec::new());
3854 }
3855
3856 #[test]
3857 fn kind_round_trips_through_its_published_name() {
3858 for k in [
3862 Kind::Doc,
3863 Kind::Para,
3864 Kind::Heading,
3865 Kind::Container,
3866 Kind::TaskListItem,
3867 Kind::Superscript,
3868 Kind::FootnoteReference,
3869 Kind::ProcessingInstruction,
3870 Kind::Cdata,
3871 ] {
3872 assert_eq!(Kind::from(k.as_str()), k, "{k} did not round-trip");
3873 assert!(!k.is_unknown());
3874 }
3875 }
3876
3877 #[test]
3878 fn an_unknown_kind_name_is_carried_rather_than_lost() {
3879 let k = Kind::from("some_future_kind");
3882 assert!(k.is_unknown());
3883 assert_eq!(k.as_str(), "some_future_kind");
3884 assert_eq!(k, Kind::Other("some_future_kind".to_string()));
3885 }
3886
3887 #[test]
3888 fn every_kind_the_library_publishes_has_a_variant() {
3889 let cases: &[(&str, Format, MarkdownExtensions)] = &[
3894 (
3895 "# 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",
3896 Format::Markdown,
3897 MarkdownExtensions {
3898 directives: false,
3899 math: false,
3900 html_elements: false,
3901 highlight: false,
3902 highlight_colors: false,
3903 },
3904 ),
3905 (
3906 "| 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",
3907 Format::Markdown,
3908 MarkdownExtensions::default(),
3909 ),
3910 (
3911 ":::note\nbody\n:::\n\n:role[x]\n\n$a+b$ ==h== ==🔴 r==\n",
3912 Format::Markdown,
3913 MarkdownExtensions {
3914 directives: true,
3915 math: true,
3916 html_elements: false,
3917 highlight: true,
3918 highlight_colors: true,
3919 },
3920 ),
3921 (
3922 "a^b^ c~d~ {=e=} {+f+} {-g-} 'q' \"dq\"\n\n\n\n<https://e.com>\n",
3923 Format::Djot,
3924 MarkdownExtensions::default(),
3925 ),
3926 (
3927 "<!-- c --><!DOCTYPE html><video controls><p>x</p></video>",
3928 Format::Html,
3929 MarkdownExtensions::default(),
3930 ),
3931 ];
3932
3933 let mut unknown: Vec<String> = Vec::new();
3934 let mut seen: Vec<String> = Vec::new();
3935 for (src, format, ext) in cases {
3936 let mut ed = Editor::new_ext(src.as_bytes(), *format, *ext).expect("editor");
3937 for n in ed.nodes().expect("nodes") {
3938 if n.kind.is_unknown() {
3939 unknown.push(n.kind.as_str().to_string());
3940 }
3941 seen.push(n.kind.as_str().to_string());
3942 }
3943 }
3944 unknown.sort();
3945 unknown.dedup();
3946 assert!(unknown.is_empty(), "kinds with no variant: {unknown:?}");
3947
3948 seen.sort();
3951 seen.dedup();
3952 assert!(
3953 seen.len() >= 30,
3954 "only {} distinct kinds reached: {seen:?}",
3955 seen.len()
3956 );
3957 }
3958
3959 #[test]
3960 fn diagnostics_report_what_a_conversion_would_lose() {
3961 let mut doc = Document::parse_str("a^b^ c\n", Format::Djot).expect("parse djot");
3965
3966 let to_md = doc
3967 .diagnostics(Target::Markdown)
3968 .expect("markdown diagnostics");
3969 assert_eq!(
3970 to_md,
3971 vec![Warning {
3972 fidelity: Fidelity::Degraded,
3973 path: "0/1".to_string(),
3974 kind: Kind::Superscript,
3975 }]
3976 );
3977
3978 assert_eq!(
3980 doc.diagnostics(Target::Djot).expect("djot diagnostics"),
3981 Vec::new()
3982 );
3983 }
3984
3985 #[test]
3986 fn diagnostics_separate_a_droppable_node_from_a_degradable_one() {
3987 let mut doc =
3991 Document::parse_str("<p>hi</p><!-- secret -->", Format::Html).expect("parse html");
3992 let warnings = doc.diagnostics(Target::Djot).expect("djot diagnostics");
3993 let comment = warnings
3994 .iter()
3995 .find(|w| w.kind == Kind::Comment)
3996 .expect("a warning about the comment");
3997 assert_eq!(comment.fidelity, Fidelity::Dropped);
3998 }
3999
4000 #[test]
4001 fn diagnostics_refuse_a_target_with_no_serializer() {
4002 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
4005 assert_eq!(doc.diagnostics(Target::Xml), Err(Error::UnsupportedFormat));
4006 assert!(doc.diagnostics(Target::Asciidoc).is_ok());
4008 }
4009
4010 #[test]
4011 fn diagnostics_flag_a_header_less_table_and_leave_a_headed_one_alone() {
4012 let mut headed = Document::parse_str(
4017 "<table><tr><th>H</th></tr><tr><td>a</td></tr></table>",
4018 Format::Html,
4019 )
4020 .expect("parse headed table");
4021 assert!(
4022 headed
4023 .diagnostics(Target::Markdown)
4024 .expect("diagnostics")
4025 .iter()
4026 .all(|w| w.kind != Kind::Table)
4027 );
4028
4029 let mut headless = Document::parse_str("<table><tr><td>a</td></tr></table>", Format::Html)
4030 .expect("parse header-less table");
4031 let table_warning = headless
4032 .diagnostics(Target::Markdown)
4033 .expect("diagnostics")
4034 .into_iter()
4035 .find(|w| w.kind == Kind::Table)
4036 .expect("a warning about the table");
4037 assert_eq!(table_warning.fidelity, Fidelity::Degraded);
4038 }
4039
4040 #[test]
4041 fn container_origin_separates_a_div_from_a_div() {
4042 let mut html =
4047 Editor::new("<div>hi</div>\n".as_bytes(), Format::Html).expect("html editor");
4048 let mut md = Editor::new_ext(
4049 ":::div\nhi\n:::\n".as_bytes(),
4050 Format::Markdown,
4051 MarkdownExtensions {
4052 directives: true,
4053 ..Default::default()
4054 },
4055 )
4056 .expect("markdown editor");
4057
4058 let html_nodes = html.nodes().expect("html nodes");
4059 let md_nodes = md.nodes().expect("markdown nodes");
4060 let tag = html_nodes
4061 .iter()
4062 .find(|n| n.name.as_deref() == Some("div"))
4063 .expect("a <div> container");
4064 let directive = md_nodes
4065 .iter()
4066 .find(|n| n.name.as_deref() == Some("div"))
4067 .expect("a :::div container");
4068
4069 assert_eq!(tag.kind, directive.kind);
4071 assert_eq!(tag.name, directive.name);
4072 assert_eq!(tag.directive_form, directive.directive_form);
4073 assert_eq!(tag.directive_form, Some(DirectiveForm::Container));
4074
4075 assert_eq!(tag.origin, Some(ContainerOrigin::Element));
4077 assert_eq!(directive.origin, Some(ContainerOrigin::Directive));
4078 }
4079
4080 fn for_both_formats(src: &str, check: impl Fn(&mut Document, Format)) {
4084 for format in [Format::Markdown, Format::Djot] {
4085 let mut doc = Document::parse(src.as_bytes(), format).expect("parse");
4086 check(&mut doc, format);
4087 }
4088 }
4089
4090 #[test]
4091 fn marker_span_is_what_a_rich_view_hides() {
4092 for_both_formats("> - [x] done\n", |doc, format| {
4093 let nodes = doc.nodes().expect("nodes");
4094 let quote = nodes
4095 .iter()
4096 .find(|n| n.kind == Kind::BlockQuote)
4097 .expect("a block quote");
4098 let item = nodes
4099 .iter()
4100 .find(|n| n.kind == Kind::TaskListItem)
4101 .expect("a task item");
4102
4103 assert_eq!(quote.marker_span, Some(0..2), "{format:?}");
4107 assert_eq!(item.marker_span, Some(2..8), "{format:?}");
4108
4109 assert_eq!(quote.content_span, Some(quote.span.clone()), "{format:?}");
4113
4114 let para = nodes
4116 .iter()
4117 .find(|n| n.kind == Kind::Para)
4118 .expect("a paragraph");
4119 assert_eq!(para.marker_span, None, "{format:?}");
4120 });
4121 }
4122
4123 #[test]
4124 fn attrs_span_locates_the_attribute_block_a_heuristic_had_to_guess_at() {
4125 let src = "{.vis .family}\nheld back\n\nplain\n";
4131 let mut doc = Document::parse(src.as_bytes(), Format::Djot).expect("parse");
4132 let nodes = doc.nodes().expect("nodes");
4133 let paras: Vec<&FlatNode> = nodes.iter().filter(|n| n.kind == Kind::Para).collect();
4134 assert_eq!(paras.len(), 2);
4135
4136 let span = doc
4137 .attrs_span(paras[0].id)
4138 .expect("attrs span")
4139 .expect("the attributed paragraph has one");
4140 assert_eq!(&src[span.clone()], "{.vis .family}");
4141 assert!(span.end <= paras[0].span.start);
4144
4145 assert_eq!(doc.attrs_span(paras[1].id).expect("attrs span"), None);
4148 }
4149
4150 #[test]
4151 fn line_prefix_assembles_every_marker_on_the_line() {
4152 for_both_formats("> - [x] done\n", |doc, format| {
4153 assert_eq!(doc.line_prefix(9).expect("prefix"), Some(0..8), "{format:?}");
4156 });
4157 }
4158
4159 #[test]
4160 fn line_prefix_is_none_on_a_continuation_line() {
4161 for_both_formats("> c\n> d\n", |doc, format| {
4167 assert_eq!(doc.line_prefix(2).expect("prefix"), Some(0..2), "{format:?}");
4168 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
4169 });
4170 }
4171
4172 #[test]
4173 fn a_caret_at_a_blocks_end_is_in_that_block_in_both_formats() {
4174 for_both_formats("a\n\nb\n", |doc, format| {
4180 for offset in [0usize, 1, 3, 4] {
4181 let hit = doc
4182 .node_at_caret(offset)
4183 .expect("caret hit")
4184 .expect("some node");
4185 assert_eq!(hit.kind, Kind::Str, "{format:?} at {offset}");
4186 }
4187 for offset in [2usize, 5] {
4190 let hit = doc
4191 .node_at_caret(offset)
4192 .expect("caret hit")
4193 .expect("some node");
4194 assert_eq!(hit.kind, Kind::Doc, "{format:?} at {offset}");
4195 }
4196 });
4197 }
4198
4199 #[test]
4200 fn the_caret_chain_ends_at_the_node_the_scalar_call_returns() {
4201 for_both_formats("- a\n", |doc, format| {
4202 let hit = doc.node_at_caret(3).expect("hit").expect("some node");
4203 let chain = doc.ancestors_at_caret(3).expect("chain");
4204 assert_eq!(chain.last().map(|m| m.node_id), Some(hit.node_id), "{format:?}");
4205 assert!(
4208 chain.iter().any(|m| m.kind == Kind::ListItem),
4209 "{format:?}: chain should reach the list item"
4210 );
4211 });
4212 }
4213
4214 #[test]
4215 fn continuation_prefix_repeats_a_quote_and_indents_past_an_item() {
4216 for_both_formats("> - a\n", |doc, format| {
4217 assert_eq!(doc.line_prefix(4).expect("prefix"), Some(0..4), "{format:?}");
4222 let cont = doc.continuation_prefix(4).expect("continuation");
4223 assert_eq!(cont.text, "> ", "{format:?}");
4224 assert_eq!(cont.columns, 4, "{format:?}");
4225 });
4226 }
4227
4228 #[test]
4229 fn continuation_prefix_answers_on_a_line_that_opens_nothing() {
4230 for_both_formats("> c\n> d\n", |doc, format| {
4233 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
4234 assert_eq!(
4235 doc.continuation_prefix(6).expect("continuation").text,
4236 "> ",
4237 "{format:?}"
4238 );
4239 });
4240 }
4241
4242 #[test]
4243 fn continuation_prefix_takes_an_ordered_markers_own_width() {
4244 for_both_formats("10. x\n", |doc, format| {
4247 assert_eq!(
4248 doc.continuation_prefix(4).expect("continuation").columns,
4249 4,
4250 "{format:?}"
4251 );
4252 });
4253 for_both_formats("1. x\n", |doc, format| {
4254 assert_eq!(
4255 doc.continuation_prefix(3).expect("continuation").columns,
4256 3,
4257 "{format:?}"
4258 );
4259 });
4260 }
4261
4262 #[test]
4263 fn a_blank_line_keeps_a_quote_alive_and_drops_an_items_indent() {
4264 for_both_formats("> - a\n", |doc, format| {
4265 let blank = doc.blank_line_prefix(4).expect("blank");
4266 assert_eq!(blank.text, ">", "{format:?}");
4269 assert_eq!(blank.columns, 1, "{format:?}");
4270 });
4271 for_both_formats("- a\n", |doc, format| {
4274 assert_eq!(doc.blank_line_prefix(3).expect("blank").text, "", "{format:?}");
4275 });
4276 }
4277
4278 #[test]
4279 fn a_prefix_column_count_is_not_its_byte_length() {
4280 let mut doc = Document::parse("- x
4283".as_bytes(), Format::Markdown).expect("parse");
4284 let cont = doc.continuation_prefix(2).expect("continuation");
4285 assert_eq!(cont.columns, 4);
4286 }
4287
4288 #[test]
4289 fn set_block_opens_a_heading_on_a_blank_line() {
4290 for format in [Format::Markdown, Format::Djot] {
4291 let mut ed = Editor::new("a\n\n".as_bytes(), format).expect("editor");
4292 ed.set_block(3, BlockKind::Heading(2)).expect("set_block");
4293 assert_eq!(ed.source().expect("source"), b"a\n\n## ", "{format:?}");
4294 let nodes = ed.nodes().expect("nodes");
4298 assert!(
4299 nodes.iter().any(|n| n.kind == Kind::Heading),
4300 "{format:?}: should have parsed a heading"
4301 );
4302 }
4303 }
4304
4305 #[test]
4306 fn set_block_refuses_a_blank_line_inside_a_code_block() {
4307 for format in [Format::Markdown, Format::Djot] {
4311 let src = "```\nx\n\ny\n```\n";
4312 let mut ed = Editor::new(src.as_bytes(), format).expect("editor");
4313 let blank = src.find("\n\n").expect("a blank line") + 1;
4314 assert!(
4315 matches!(
4316 ed.set_block(blank, BlockKind::Heading(1)),
4317 Err(Error::NotEditable)
4318 ),
4319 "{format:?}"
4320 );
4321 assert_eq!(ed.source().expect("source"), src.as_bytes(), "{format:?}");
4322 }
4323 }
4324
4325 #[test]
4326 fn task_items_report_their_checkbox_state() {
4327 for_both_formats("- [ ] a\n- [x] b\n- [X] c\n- d\n", |doc, format| {
4331 let nodes = doc.nodes().expect("nodes");
4332 let states: Vec<Option<bool>> = nodes
4333 .iter()
4334 .filter(|n| matches!(n.kind, Kind::TaskListItem | Kind::ListItem))
4335 .map(|n| n.checked)
4336 .collect();
4337 assert_eq!(
4338 states,
4339 vec![Some(false), Some(true), Some(true), None],
4340 "{format:?}"
4341 );
4342
4343 for n in nodes.iter().filter(|n| n.kind == Kind::Para) {
4346 assert_eq!(n.checked, None, "{format:?}");
4347 }
4348 });
4349 }
4350
4351 #[test]
4352 fn an_editor_reaches_the_caret_reads_through_its_document_view() {
4353 let mut ed = Editor::new("- a\n".as_bytes(), Format::Markdown).expect("editor");
4358 let mut view = ed.document().expect("document view");
4359
4360 assert_eq!(view.line_prefix(3).expect("prefix"), Some(0..2));
4361 let hit = view.node_at_caret(3).expect("hit").expect("some node");
4362 assert_eq!(hit.kind, Kind::Str);
4363 }
4364
4365 #[test]
4366 fn container_origin_is_none_for_non_containers() {
4367 let mut ed = Editor::new("# hi\n\npara\n".as_bytes(), Format::Markdown).expect("editor");
4370 for n in ed.nodes().expect("nodes") {
4371 assert_eq!(n.origin, None, "{} should carry no origin", n.kind);
4372 }
4373 }
4374
4375 #[test]
4376 fn flat_nodes_expose_directive_name_and_form() {
4377 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
4383 let mut ed = Editor::new_ext(
4384 src.as_bytes(),
4385 Format::Markdown,
4386 MarkdownExtensions {
4387 directives: true,
4388 ..Default::default()
4389 },
4390 )
4391 .expect("editor");
4392 let nodes = ed.nodes().expect("nodes");
4393
4394 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
4395 .iter()
4396 .filter(|n| n.kind == Kind::Container)
4397 .map(|n| (n.name.as_deref(), n.directive_form))
4398 .collect();
4399 assert_eq!(
4400 forms,
4401 vec![
4402 (Some("note"), Some(DirectiveForm::Container)),
4403 (Some("embed"), Some(DirectiveForm::Leaf)),
4404 (Some("abbr"), Some(DirectiveForm::Text)),
4405 ]
4406 );
4407
4408 let embed = nodes
4411 .iter()
4412 .find(|n| n.name.as_deref() == Some("embed"))
4413 .expect("embed");
4414 assert_eq!(
4415 embed.attrs,
4416 vec![("src".to_string(), Some("demo.html".to_string()))]
4417 );
4418 let para = nodes.iter().find(|n| n.kind == Kind::Para).expect("a para");
4419 assert!(para.directive_form.is_none() && para.name.is_none());
4420 }
4421
4422 #[test]
4423 fn editor_insert_child_and_delete() {
4424 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
4425 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4426 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
4427 ed.delete("0.1").expect("delete");
4428 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
4429 }
4430
4431 #[test]
4432 fn editor_edits_by_selector() {
4433 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4434 ed.replace("heading(\"Two\")", "## Renamed")
4435 .expect("replace");
4436 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
4437 }
4438
4439 #[test]
4440 fn editor_locator_errors_are_distinct() {
4441 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
4442 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
4443 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
4444 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
4445 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
4447 }
4448
4449 #[test]
4450 fn editor_reparse_break_rolls_back() {
4451 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
4452 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
4453 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
4454 }
4455
4456 #[test]
4457 fn editor_leaf_content_is_not_editable() {
4458 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4459 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
4460 }
4461
4462 #[test]
4463 fn editor_query_reflects_current_tree() {
4464 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
4465 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4466 assert_eq!(ed.query("element").expect("query").len(), 3);
4468 let json = ed.ast_json().expect("ast_json");
4469 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
4470 }
4471
4472 #[test]
4475 fn editor_edit_range_types_backspaces_and_reports_change() {
4476 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4477
4478 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
4480 assert_eq!(ed.source_str().unwrap(), "aXb\n");
4481 assert_eq!(c.old, 1..1);
4482 assert_eq!(c.new, 1..2);
4483 assert_eq!(c.delta(), 1);
4484
4485 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
4487 assert_eq!(ed.source_str().unwrap(), "ab\n");
4488 assert_eq!(c2.old, 1..2);
4489 assert_eq!(c2.new, 1..1);
4490 assert_eq!(c2.delta(), -1);
4491 }
4492
4493 #[test]
4494 fn editor_edit_range_rejects_bad_ranges() {
4495 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4496 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"); }
4500
4501 #[test]
4502 fn editor_last_change_reports_locator_ops_too() {
4503 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4504 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed")
4507 .expect("replace");
4508 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
4509 let c = ed.last_change().expect("a change was recorded");
4510 assert_eq!(c.old, 7..13);
4512 assert_eq!(c.new, 7..17);
4513 }
4514
4515 #[test]
4516 fn editor_nodes_is_a_walkable_flat_tree() {
4517 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4518 let nodes = ed.nodes().expect("nodes");
4519 assert!(!nodes.is_empty());
4520
4521 for (i, n) in nodes.iter().enumerate() {
4523 assert_eq!(n.id, NodeId(i as u32));
4524 }
4525 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
4527 assert_eq!(roots.len(), 1);
4528 assert_eq!(roots[0].kind, Kind::Doc);
4529
4530 let heading = nodes
4532 .iter()
4533 .find(|n| n.kind == Kind::Heading)
4534 .expect("a heading");
4535 assert_eq!(heading.level, Some(1));
4536 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
4537
4538 assert_eq!(heading.head, None);
4540 assert_eq!(heading.alignment, None);
4541
4542 for n in nodes.iter().filter(|n| n.parent.is_some()) {
4545 let p = &nodes[n.parent.unwrap().0 as usize];
4546 let mut kid = p.first_child;
4547 let mut seen = false;
4548 while let Some(NodeId(k)) = kid {
4549 if k == n.id.0 {
4550 seen = true;
4551 break;
4552 }
4553 kid = nodes[k as usize].next_sibling;
4554 }
4555 assert!(
4556 seen,
4557 "node {:?} not found among its parent's children",
4558 n.id
4559 );
4560 }
4561 }
4562
4563 #[test]
4564 fn editor_child_spans_and_subtree_agree_with_nodes() {
4565 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
4566 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4567 let all = ed.nodes().expect("nodes");
4568 let doc = all.iter().find(|n| n.kind == Kind::Doc).expect("doc");
4569
4570 let top = ed.child_spans(None).expect("child_spans");
4573 let mut want = Vec::new();
4574 let mut c = doc.first_child;
4575 while let Some(id) = c {
4576 want.push(id);
4577 c = all[id.0 as usize].next_sibling;
4578 }
4579 assert_eq!(top.len(), want.len(), "top-level count");
4580 for (m, id) in top.iter().zip(&want) {
4581 assert_eq!(m.node_id, id.0, "child id");
4582 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
4583 assert_eq!(m.span, all[id.0 as usize].span, "child span");
4584 }
4585 assert!(
4587 src[top[0].span.clone()].starts_with('#'),
4588 "first block is the heading"
4589 );
4590
4591 let list = top
4593 .iter()
4594 .find(|m| {
4595 matches!(
4596 m.kind,
4597 Kind::BulletList | Kind::OrderedList | Kind::TaskList
4598 )
4599 })
4600 .expect("a list");
4601 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
4602 assert_eq!(items.len(), 2);
4603 assert!(
4604 items.iter().all(|m| m.kind == Kind::ListItem),
4605 "items: {items:?}"
4606 );
4607
4608 let para = top
4610 .iter()
4611 .find(|m| m.kind == Kind::Para)
4612 .expect("a para")
4613 .node_id;
4614 let sub = ed.subtree(NodeId(para)).expect("subtree");
4615 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
4616 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
4617 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
4618 assert_eq!(sub[0].kind, Kind::Para);
4619 for (i, n) in sub.iter().enumerate() {
4620 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
4621 for link in [n.parent, n.first_child, n.next_sibling]
4622 .into_iter()
4623 .flatten()
4624 {
4625 assert!(
4626 (link.0 as usize) < sub.len(),
4627 "link {link:?} escapes the subtree"
4628 );
4629 }
4630 }
4631 assert!(
4632 src[sub[0].span.clone()].starts_with("Hello"),
4633 "absolute span: {:?}",
4634 &src[sub[0].span.clone()]
4635 );
4636
4637 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<Kind> {
4639 let mut out = Vec::new();
4640 let mut stack = vec![root];
4641 while let Some(id) = stack.pop() {
4642 let n = &all[id.0 as usize];
4643 out.push(n.kind.clone());
4644 let mut c = n.first_child;
4645 while let Some(cid) = c {
4646 stack.push(cid);
4647 c = all[cid.0 as usize].next_sibling;
4648 }
4649 }
4650 out
4651 }
4652 let mut want_kinds = arena_kinds(&all, NodeId(para));
4653 let mut got_kinds: Vec<Kind> = sub.iter().map(|n| n.kind.clone()).collect();
4654 want_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4658 got_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4659 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
4660
4661 assert!(matches!(
4663 ed.subtree(NodeId(9999)),
4664 Err(Error::InvalidArgument)
4665 ));
4666 }
4667
4668 #[test]
4669 fn flat_nodes_carry_table_head_and_alignment() {
4670 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
4674 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4675 let nodes = ed.nodes().expect("nodes");
4676
4677 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Row).collect();
4678 assert_eq!(rows.len(), 2, "a header row and one body row");
4679 assert_eq!(rows[0].head, Some(true), "first row is the header");
4680 assert_eq!(rows[1].head, Some(false), "second row is a body row");
4681
4682 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Cell).collect();
4683 assert_eq!(cells.len(), 4);
4684 assert_eq!(cells[0].alignment, Some(Alignment::Left));
4686 assert_eq!(cells[1].alignment, Some(Alignment::Right));
4687 assert_eq!(cells[2].alignment, Some(Alignment::Left));
4688 assert_eq!(cells[3].alignment, Some(Alignment::Right));
4689 assert_eq!(cells[0].head, Some(true));
4691 assert_eq!(cells[2].head, Some(false));
4692
4693 let mut plain =
4696 Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
4697 let pnodes = plain.nodes().expect("nodes");
4698 let pcell = pnodes
4699 .iter()
4700 .find(|n| n.kind == Kind::Cell)
4701 .expect("a cell");
4702 assert_eq!(pcell.alignment, Some(Alignment::Default));
4703 }
4704
4705 #[test]
4706 fn cell_extent_reports_merged_cells_and_nothing_else() {
4707 let src = "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td><td>b</td></tr></table>";
4708 let mut doc = Document::parse_str(src, Format::Html).expect("parse");
4709 let cells: Vec<NodeId> = doc
4710 .nodes()
4711 .expect("nodes")
4712 .iter()
4713 .filter(|n| n.kind == Kind::Cell)
4714 .map(|n| n.id)
4715 .collect();
4716 assert_eq!(cells.len(), 2);
4717 assert_eq!(doc.cell_extent(cells[0]).expect("extent"), Some((2, 3)));
4718 assert_eq!(doc.cell_extent(cells[1]).expect("extent"), Some((1, 1)));
4720
4721 let mut pipe =
4723 Document::parse_str("| a |\n| --- |\n| b |\n", Format::Markdown).expect("parse");
4724 let pipe_cell = pipe
4725 .nodes()
4726 .expect("nodes")
4727 .iter()
4728 .find(|n| n.kind == Kind::Cell)
4729 .expect("a cell")
4730 .id;
4731 assert_eq!(pipe.cell_extent(pipe_cell).expect("extent"), Some((1, 1)));
4732
4733 let root = NodeId(0);
4735 assert_eq!(pipe.cell_extent(root).expect("extent"), None);
4736 }
4737
4738 #[test]
4739 fn builder_add_cell_spanning_renders_colspan_and_rowspan() {
4740 let mut b = Builder::new().expect("builder");
4741 let wide_text = b.add_text(TextKind::Str, "wide").expect("str");
4742 let wide = b
4743 .add_cell_spanning(false, Alignment::Default, 2, 3)
4744 .expect("cell");
4745 b.set_children(wide, &[wide_text]).expect("children");
4746 let plain_text = b.add_text(TextKind::Str, "one").expect("str");
4747 let plain = b.add_cell(false, Alignment::Default).expect("cell");
4748 b.set_children(plain, &[plain_text]).expect("children");
4749 let row = b.add_row(false).expect("row");
4750 b.set_children(row, &[wide, plain]).expect("children");
4751 let table = b.add(VoidKind::Table).expect("table");
4752 b.set_children(table, &[row]).expect("children");
4753
4754 let html = String::from_utf8(b.render_html(table).expect("html")).expect("utf-8");
4755 assert!(
4756 html.contains("<td colspan=\"2\" rowspan=\"3\">wide</td>"),
4757 "{html}"
4758 );
4759 assert!(html.contains("<td>one</td>"), "{html}");
4761
4762 assert!(matches!(
4764 b.add_cell_spanning(false, Alignment::Default, 0, 1),
4765 Err(Error::InvalidArgument)
4766 ));
4767 }
4768
4769 #[test]
4770 fn editor_node_at_and_ancestors_hit_test_offsets() {
4771 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4772
4773 let m = ed
4775 .node_at(2)
4776 .expect("node_at")
4777 .expect("a node covers offset 2");
4778 assert!(m.span.contains(&2));
4779
4780 let chain = ed.ancestors_at(2).expect("ancestors_at");
4782 assert!(!chain.is_empty());
4783 assert_eq!(chain[0].kind, Kind::Doc);
4784 assert_eq!(chain.last().unwrap().node_id, m.node_id);
4785
4786 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
4788 }
4789
4790 #[test]
4793 fn editor_wrap_and_toggle_inline_round_trip() {
4794 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4795
4796 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
4798 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
4799 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
4800
4801 ed.toggle_inline(4, 8, InlineKind::Strong)
4803 .expect("toggle off");
4804 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4805
4806 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
4808 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
4809 }
4810
4811 #[test]
4812 fn editor_inline_marks_cut_at_block_boundaries() {
4813 let mut ed = Editor::new_str("one two\n\nthree four\n", Format::Markdown)
4816 .expect("editor");
4817 let c = ed.toggle_inline(0, 19, InlineKind::Strong).expect("toggle on");
4818 assert_eq!(
4819 ed.source_str().unwrap(),
4820 "**one two**\n\n**three four**\n"
4821 );
4822
4823 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**one two**\n\n**three four**");
4826 ed.undo().expect("undo");
4827 assert_eq!(ed.source_str().unwrap(), "one two\n\nthree four\n");
4828
4829 ed.toggle_inline(0, 19, InlineKind::Strong).expect("toggle on");
4832 ed.toggle_inline(0, 27, InlineKind::Strong).expect("toggle off");
4833 assert_eq!(ed.source_str().unwrap(), "one two\n\nthree four\n");
4834
4835 let mut fenced = Editor::new_str("```\nx y\n```\n", Format::Markdown).expect("editor");
4837 assert_eq!(
4838 fenced.toggle_inline(4, 7, InlineKind::Strong),
4839 Err(Error::NotEditable)
4840 );
4841 }
4842
4843 #[test]
4844 fn editor_inline_kind_support_is_format_specific() {
4845 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4847 assert_eq!(
4848 md.wrap_range(2, 6, InlineKind::Mark),
4849 Err(Error::UnsupportedFormat)
4850 );
4851
4852 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4854 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
4855 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
4856 }
4857
4858 #[test]
4859 fn editor_authors_gfm_strikethrough_out_of_the_box() {
4860 assert!(Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Delete)));
4864 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4865 ed.toggle_inline(2, 6, InlineKind::Delete).expect("strike");
4866 assert_eq!(ed.source_str().unwrap(), "a ~~word~~ b\n");
4867 ed.toggle_inline(4, 8, InlineKind::Delete).expect("unstrike");
4868 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4869 }
4870
4871 #[test]
4872 fn editor_highlight_is_authorable_with_the_extension_on() {
4873 let exts = MarkdownExtensions {
4874 highlight: true,
4875 ..Default::default()
4876 };
4877 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
4881 assert!(Format::Markdown.supports_with(exts, Gesture::ToggleInline(InlineKind::Mark)));
4882
4883 let mut ed =
4884 Editor::new_ext(b"a word b\n", Format::Markdown, exts).expect("editor");
4885 ed.toggle_inline(2, 6, InlineKind::Mark).expect("highlight");
4886 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4887 ed.toggle_inline(4, 8, InlineKind::Mark).expect("unhighlight");
4888 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4889 }
4890
4891 #[test]
4892 fn editor_set_mark_color_writes_reads_and_clears_the_colour() {
4893 let exts = MarkdownExtensions {
4894 highlight: true,
4895 highlight_colors: true,
4896 ..Default::default()
4897 };
4898 assert!(Format::Markdown.supports_with(exts, Gesture::SetMarkColor));
4899 let hi_only = MarkdownExtensions {
4901 highlight: true,
4902 ..Default::default()
4903 };
4904 assert!(!Format::Markdown.supports_with(hi_only, Gesture::SetMarkColor));
4905 assert!(!Format::Markdown.supports(Gesture::SetMarkColor));
4906 assert!(!Format::Djot.supports_with(exts, Gesture::SetMarkColor));
4907
4908 let mut ed =
4909 Editor::new_ext("a ==word== b\n".as_bytes(), Format::Markdown, exts).expect("editor");
4910 ed.set_mark_color(6, Some(MarkColor::Red)).expect("colour");
4911 assert_eq!(ed.source_str().unwrap(), "a ==\u{1F534} word== b\n");
4912
4913 let mut doc =
4915 Document::parse_with(ed.source_str().unwrap().as_bytes(), Format::Markdown, exts)
4916 .expect("parse");
4917 assert_eq!(doc.query("mark[data-color=red]").expect("query").len(), 1);
4918
4919 ed.set_mark_color(9, Some(MarkColor::Blue)).expect("recolour");
4920 assert_eq!(ed.source_str().unwrap(), "a ==\u{1F535} word== b\n");
4921 ed.set_mark_color(9, None).expect("clear");
4922 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4923
4924 assert_eq!(
4926 ed.set_mark_color(0, Some(MarkColor::Red)),
4927 Err(Error::NotEditable)
4928 );
4929 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4930
4931 for c in [
4933 MarkColor::Red,
4934 MarkColor::Orange,
4935 MarkColor::Yellow,
4936 MarkColor::Green,
4937 MarkColor::Blue,
4938 MarkColor::Purple,
4939 MarkColor::Brown,
4940 ] {
4941 assert_eq!(MarkColor::from_str(c.as_str()), Some(c));
4942 }
4943 assert_eq!(MarkColor::from_str("pink"), None);
4944 }
4945
4946 #[test]
4947 fn editor_toggle_strips_verbatim_via_content_span() {
4948 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
4949 ed.toggle_inline(2, 8, InlineKind::Verbatim)
4951 .expect("toggle code off");
4952 assert_eq!(ed.source_str().unwrap(), "a code b\n");
4953
4954 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
4957 ed2.toggle_inline(2, 7, InlineKind::Verbatim)
4958 .expect("toggle multi off");
4959 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
4960 }
4961
4962 #[test]
4963 fn editor_set_block_switches_para_and_heading_levels() {
4964 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
4965
4966 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
4968 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
4969
4970 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
4972 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
4973
4974 ed.set_block(2, BlockKind::Paragraph).expect("to para");
4976 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
4977 }
4978
4979 #[test]
4980 fn editor_set_block_rejects_bad_level_and_format() {
4981 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4982 assert_eq!(
4983 md.set_block(0, BlockKind::Heading(9)),
4984 Err(Error::InvalidArgument)
4985 );
4986
4987 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4988 assert_eq!(
4989 xml.set_block(1, BlockKind::Heading(1)),
4990 Err(Error::UnsupportedFormat)
4991 );
4992 }
4993
4994 #[test]
4995 fn editor_toggle_block_container_round_trips() {
4996 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
4997
4998 let c = ed
4999 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
5000 .expect("quote on");
5001 assert_eq!(ed.source_str().unwrap(), "> a\n");
5002 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
5003
5004 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
5005 .expect("quote off");
5006 assert_eq!(ed.source_str().unwrap(), "a\n");
5007 }
5008
5009 #[test]
5010 fn editor_toggle_block_container_nests_a_partial_selection() {
5011 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
5012
5013 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
5016 .expect("nest");
5017 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
5018
5019 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
5021 .expect("peel");
5022 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
5023 }
5024
5025 #[test]
5026 fn editor_toggle_block_container_numbers_and_converts_lists() {
5027 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
5028
5029 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
5031 .expect("ordered on");
5032 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
5033
5034 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
5036 .expect("convert");
5037 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
5038 }
5039
5040 #[test]
5041 fn editor_toggle_block_container_rejects_unspellable_format() {
5042 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5043 assert_eq!(
5044 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
5045 Err(Error::UnsupportedFormat)
5046 );
5047 }
5048
5049 #[test]
5050 fn editor_insert_link_wraps_and_repoints() {
5051 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
5052
5053 ed.insert_link(2, 6, "http://x.dev").expect("link");
5054 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
5055
5056 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
5058 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
5059 }
5060
5061 #[test]
5062 fn editor_insert_link_repoints_an_autolink() {
5063 for format in [Format::Markdown, Format::Djot] {
5068 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
5069 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
5070 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
5071
5072 let nodes = ed.nodes().expect("nodes");
5074 let url = nodes
5075 .iter()
5076 .find(|n| n.kind == Kind::Url)
5077 .expect("still an autolink");
5078 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
5079 assert!(!nodes.iter().any(|n| n.kind == Kind::Link));
5080 }
5081 }
5082
5083 #[test]
5084 fn editor_insert_link_escapes_the_destination() {
5085 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
5088 dj.insert_link(0, 1, "a)b").expect("link");
5089 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
5090
5091 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
5095 md.insert_link(0, 1, "a b").expect("link");
5096 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
5097
5098 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
5099 dj2.insert_link(0, 1, "a b").expect("link");
5100 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
5101 }
5102
5103 #[test]
5104 fn editor_insert_image_escapes_the_destination_per_format() {
5105 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
5108 md.insert_image(0, 1, "my cat.png").expect("image");
5109 assert_eq!(md.source_str().unwrap(), "\n");
5110
5111 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
5112 dj.insert_image(0, 1, "my cat.png").expect("image");
5113 assert_eq!(dj.source_str().unwrap(), "\n");
5114
5115 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
5117 paren.insert_image(0, 1, "a)b.png").expect("image");
5118 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
5119 }
5120
5121 #[test]
5122 fn editor_insert_image_keeps_an_empty_alt_empty() {
5123 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5126 ed.insert_image(1, 1, "cat.png").expect("image");
5127 assert_eq!(ed.source_str().unwrap(), "ab\n");
5128 }
5129
5130 #[test]
5131 fn editor_insert_image_rejects_a_newline_destination() {
5132 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
5133 assert_eq!(
5134 ed.insert_image(0, 1, "a\nb.png"),
5135 Err(Error::InvalidArgument)
5136 );
5137
5138 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5139 assert_eq!(
5140 xml.insert_image(3, 5, "x.png"),
5141 Err(Error::UnsupportedFormat)
5142 );
5143 }
5144
5145 #[test]
5146 fn editor_insert_link_rejects_a_newline_destination() {
5147 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
5148 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
5149
5150 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5151 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
5152 }
5153
5154 #[test]
5155 fn editor_insert_literal_keeps_typed_specials_literal() {
5156 for format in [Format::Markdown, Format::Djot] {
5157 let mut ed = Editor::new_str("z\n", format).expect("editor");
5158 ed.insert_literal(0, "*hi*").expect("literal");
5160
5161 let nodes = ed.nodes().expect("nodes");
5163 assert!(
5164 !nodes
5165 .iter()
5166 .any(|n| n.kind == Kind::Emph || n.kind == Kind::Strong)
5167 );
5168 let text: String = nodes
5169 .iter()
5170 .filter(|n| n.kind == Kind::Str)
5171 .filter_map(|n| n.text.clone())
5172 .collect();
5173 assert_eq!(text, "*hi*z");
5174 }
5175 }
5176
5177 #[test]
5178 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
5179 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
5181 ed.insert_literal(1, "# ").expect("literal");
5182 assert_eq!(ed.source_str().unwrap(), "a# z\n");
5183
5184 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
5186 ed2.insert_literal(0, "# ").expect("literal");
5187 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
5188 assert!(
5189 !ed2.nodes()
5190 .expect("nodes")
5191 .iter()
5192 .any(|n| n.kind == Kind::Heading)
5193 );
5194 }
5195
5196 #[test]
5197 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
5198 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5199 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
5200
5201 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5202 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
5203 }
5204
5205 #[test]
5206 fn editor_insert_line_break_splices_in_cell_br() {
5207 let mut ed =
5208 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
5209 ed.insert_line_break(3).expect("line break");
5211 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
5212 let nodes = ed.nodes().expect("nodes");
5214 assert!(nodes.iter().any(|n| n.kind == Kind::HardBreak));
5215 assert!(!nodes.iter().any(|n| n.kind == Kind::RawInline));
5216 }
5217
5218 #[test]
5219 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
5220 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
5222 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
5223
5224 let mut dj = Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
5226 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
5227
5228 let mut ed =
5230 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
5231 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
5232 }
5233
5234 #[test]
5235 fn editor_insert_thematic_break_is_blank_separated_per_format() {
5236 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
5240 md.insert_thematic_break(0).expect("rule");
5241 assert_eq!(md.source_str().unwrap(), "a\n\n---\n");
5242 let nodes = md.nodes().expect("nodes");
5243 assert!(nodes.iter().any(|n| n.kind == Kind::ThematicBreak));
5244 assert!(!nodes.iter().any(|n| n.kind == Kind::Heading));
5245
5246 let mut dj = Editor::new_str("a\n", Format::Djot).expect("editor");
5249 dj.insert_thematic_break(0).expect("rule");
5250 assert_eq!(dj.source_str().unwrap(), "a\n\n* * *\n");
5251
5252 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5253 assert_eq!(xml.insert_thematic_break(3), Err(Error::UnsupportedFormat));
5254 }
5255
5256 #[test]
5257 fn editor_split_block_keeps_both_halves_the_same_kind() {
5258 let mut item = Editor::new_str("- this is a list item\n", Format::Markdown).expect("editor");
5261 item.split_block(10).expect("split");
5262 assert_eq!(item.source_str().unwrap(), "- this is \n- a list item\n");
5263 let nodes = item.nodes().expect("nodes");
5264 assert_eq!(nodes.iter().filter(|n| n.kind == Kind::ListItem).count(), 2);
5265
5266 let mut tail = Editor::new_str("- a\n", Format::Markdown).expect("editor");
5268 tail.split_block(3).expect("split");
5269 assert_eq!(tail.source_str().unwrap(), "- a\n- \n");
5270
5271 let mut para = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5273 para.split_block(1).expect("split");
5274 assert_eq!(para.source_str().unwrap(), "a\n\nb\n");
5275
5276 let mut table =
5278 Editor::new_str("| a | b |\n|---|---|\n| c | d |\n", Format::Markdown).expect("editor");
5279 assert_eq!(table.split_block(3), Err(Error::NotEditable));
5280
5281 let mut empty = Editor::new_str("", Format::Markdown).expect("editor");
5282 assert_eq!(empty.split_block(0), Err(Error::NotFound));
5283 }
5284
5285 #[test]
5286 fn editor_toggle_code_block_round_trips_and_measures_the_fence() {
5287 let mut ed = Editor::new_str("a\n", Format::Markdown).expect("editor");
5288 ed.toggle_code_block(0, 1, Some("zig")).expect("fence");
5289 assert_eq!(ed.source_str().unwrap(), "```zig\na\n```\n");
5290 let nodes = ed.nodes().expect("nodes");
5291 assert!(nodes.iter().any(|n| n.kind == Kind::CodeBlock));
5292
5293 ed.toggle_code_block(0, 0, None).expect("unfence");
5294 assert_eq!(ed.source_str().unwrap(), "a\n");
5295
5296 let mut runs = Editor::new_str("a ``` b\n", Format::Markdown).expect("editor");
5299 runs.toggle_code_block(0, 7, None).expect("fence");
5300 assert_eq!(runs.source_str().unwrap(), "````\na ``` b\n````\n");
5301 }
5302
5303 #[test]
5304 fn editor_toggle_code_block_refuses_inside_a_list_item() {
5305 let mut ed = Editor::new_str("- a\n- b\n", Format::Markdown).expect("editor");
5308 assert_eq!(ed.toggle_code_block(2, 3, None), Err(Error::NotEditable));
5309 assert_eq!(ed.source_str().unwrap(), "- a\n- b\n");
5310 }
5311
5312 #[test]
5313 fn editor_set_code_language_retags_clears_and_refuses() {
5314 let mut ed = Editor::new_str("```zig\na\n```\n", Format::Markdown).expect("editor");
5315 ed.set_code_language(0, Some("rust")).expect("retag");
5316 assert_eq!(ed.source_str().unwrap(), "```rust\na\n```\n");
5317
5318 ed.set_code_language(0, None).expect("clear");
5321 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
5322 ed.set_code_language(0, Some("")).expect("empty");
5323 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
5324
5325 assert_eq!(
5328 ed.set_code_language(0, Some("a b")),
5329 Err(Error::InvalidArgument)
5330 );
5331 let mut dj = Editor::new_str("```\na\n```\n", Format::Djot).expect("editor");
5333 dj.set_code_language(0, Some("a b"))
5334 .expect("djot info string");
5335 assert_eq!(dj.source_str().unwrap(), "```a b\na\n```\n");
5336
5337 let mut para = Editor::new_str("x\n", Format::Markdown).expect("editor");
5338 assert_eq!(para.set_code_language(0, Some("zig")), Err(Error::NotFound));
5339 }
5340
5341 #[test]
5342 fn editor_task_checkbox_gestures() {
5343 let mut ed = Editor::new_str("- a\n", Format::Markdown).expect("editor");
5344
5345 ed.toggle_task_item(2).expect("add box");
5348 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
5349 assert!(
5350 ed.nodes()
5351 .unwrap()
5352 .iter()
5353 .any(|n| n.kind == Kind::TaskListItem)
5354 );
5355
5356 ed.set_task_checked(6, true).expect("tick");
5357 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
5358 ed.set_task_checked(6, true).expect("no-op");
5360 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
5361
5362 ed.toggle_task_checked(6).expect("flip");
5363 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
5364
5365 ed.toggle_task_item(6).expect("remove box");
5366 assert_eq!(ed.source_str().unwrap(), "- a\n");
5367
5368 assert_eq!(ed.set_task_checked(2, true), Err(Error::NotEditable));
5371 let mut para = Editor::new_str("a\n", Format::Markdown).expect("editor");
5373 assert_eq!(para.toggle_task_item(0), Err(Error::NotFound));
5374 }
5375
5376 #[test]
5377 fn editor_insert_footnote_writes_both_halves_as_one_edit() {
5378 for format in [Format::Markdown, Format::Djot] {
5379 let mut ed = Editor::new_str("see\n", format).expect("editor");
5380 ed.insert_footnote(3, "a").expect("footnote");
5381 assert_eq!(ed.source_str().unwrap(), "see[^a]\n\n[^a]: \n");
5382
5383 let nodes = ed.nodes().expect("nodes");
5385 assert!(nodes.iter().any(|n| n.kind == Kind::FootnoteReference));
5386 assert!(nodes.iter().any(|n| n.kind == Kind::Footnote));
5387
5388 ed.undo().expect("undo");
5390 assert_eq!(ed.source_str().unwrap(), "see\n");
5391 }
5392 }
5393
5394 #[test]
5395 fn editor_insert_footnote_reuses_an_existing_definition() {
5396 let mut ed = Editor::new_str("see\n", Format::Markdown).expect("editor");
5397 ed.insert_footnote(3, "a").expect("first");
5398 ed.insert_footnote(7, "a").expect("second reference");
5399 assert_eq!(ed.source_str().unwrap(), "see[^a][^a]\n\n[^a]: \n");
5400 let defs = ed
5401 .nodes()
5402 .unwrap()
5403 .iter()
5404 .filter(|n| n.kind == Kind::Footnote)
5405 .count();
5406 assert_eq!(defs, 1);
5407
5408 assert_eq!(ed.insert_footnote(3, ""), Err(Error::InvalidArgument));
5409 assert_eq!(ed.insert_footnote(3, "a]b"), Err(Error::InvalidArgument));
5410 }
5411
5412 #[test]
5413 fn editor_undo_redo_round_trip() {
5414 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5415 ed.edit_range(5, 5, "!").expect("edit");
5416 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5417
5418 let change = ed.undo().expect("undo ok").expect("something to undo");
5419 assert_eq!(ed.source_str().unwrap(), "hello\n");
5420 assert_eq!(change.new.end, 5);
5421 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
5422
5423 ed.redo().expect("redo ok").expect("something to redo");
5424 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5425 }
5426
5427 #[test]
5428 fn editor_coalesce_folds_a_run() {
5429 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5430 ed.edit_range(0, 0, "a").expect("edit");
5431 ed.edit_range(1, 1, "b").expect("edit");
5432 ed.coalesce_last_undo().expect("coalesce");
5433 assert_eq!(ed.source_str().unwrap(), "ab\n");
5434 ed.undo().expect("undo ok").expect("something to undo");
5436 assert_eq!(ed.source_str().unwrap(), "\n");
5437 assert!(ed.undo().expect("undo ok").is_none());
5438 }
5439
5440 #[test]
5441 fn editor_revision_bumps_per_successful_mutation() {
5442 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
5443 assert_eq!(ed.revision(), 0);
5444 ed.edit_range(1, 1, "y").expect("edit");
5445 assert_eq!(ed.revision(), 1);
5446
5447 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
5449 assert_eq!(xml.revision(), 0);
5450 assert!(xml.replace_content("0", "<b>").is_err());
5451 assert_eq!(xml.revision(), 0);
5452
5453 ed.undo().expect("undo ok").expect("something to undo");
5455 assert_eq!(ed.revision(), 2);
5456 ed.redo().expect("redo ok").expect("something to redo");
5457 assert_eq!(ed.revision(), 3);
5458 }
5459
5460 #[test]
5461 fn editor_dirty_range_tracks_and_clears() {
5462 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
5463 assert_eq!(ed.dirty_range(), None);
5465
5466 ed.edit_range(2, 2, "XY").expect("edit");
5468 assert_eq!(ed.dirty_range(), Some(2..4));
5469
5470 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
5474 assert!(
5475 d.start <= 2 && d.end >= 10,
5476 "range {d:?} must cover both edits"
5477 );
5478
5479 let rev = ed.revision();
5481 ed.clear_dirty();
5482 assert_eq!(ed.dirty_range(), None);
5483 assert_eq!(ed.revision(), rev);
5484
5485 ed.undo().expect("undo ok").expect("something to undo");
5487 assert!(ed.dirty_range().is_some());
5488 }
5489
5490 #[test]
5491 fn editor_caret_blob_follows_undo_and_redo() {
5492 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5493 assert!(ed.caret_blob().unwrap().is_empty());
5494
5495 ed.set_caret_blob(b"before").expect("set caret");
5497 ed.edit_range(5, 5, "!").expect("edit");
5498 assert!(ed.caret_blob().unwrap().is_empty());
5500 ed.set_caret_blob(b"after").expect("set caret");
5501
5502 ed.undo().expect("undo ok").expect("something to undo");
5504 assert_eq!(ed.source_str().unwrap(), "hello\n");
5505 assert_eq!(ed.caret_blob().unwrap(), b"before");
5506
5507 ed.redo().expect("redo ok").expect("something to redo");
5509 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5510 assert_eq!(ed.caret_blob().unwrap(), b"after");
5511 }
5512
5513 #[test]
5514 fn editor_coalesced_run_keeps_the_pre_run_caret() {
5515 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5516 ed.set_caret_blob(b"c0").expect("set caret");
5517 ed.edit_range(0, 0, "a").expect("edit");
5518 ed.set_caret_blob(b"c1").expect("set caret");
5519 ed.edit_range(1, 1, "b").expect("edit");
5520 ed.coalesce_last_undo().expect("coalesce");
5521 ed.set_caret_blob(b"c2").expect("set caret");
5522
5523 ed.undo().expect("undo ok").expect("something to undo");
5525 assert_eq!(ed.source_str().unwrap(), "\n");
5526 assert_eq!(ed.caret_blob().unwrap(), b"c0");
5527 }
5528
5529 #[test]
5530 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
5531 let mut ed = Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
5532 ed.renumber_ordered_lists(0).expect("renumber ok");
5533 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
5534 }
5535
5536 #[test]
5537 fn editor_renumber_ordered_lists_leaves_djot_prose_alone() {
5538 let src = "1. a\n 2. b\n2. c\n";
5541 let mut dj = Editor::new_str(src, Format::Djot).expect("editor");
5542 dj.renumber_ordered_lists(0).expect("renumber ok");
5543 assert_eq!(dj.source_str().unwrap(), src);
5544
5545 let mut md = Editor::new_str(src, Format::Markdown).expect("editor");
5546 md.renumber_ordered_lists(0).expect("renumber ok");
5547 assert_eq!(md.source_str().unwrap(), "1. a\n 1. b\n2. c\n");
5548 }
5549
5550 #[test]
5551 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
5552 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
5553 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
5554 }
5555
5556 #[test]
5557 fn editor_table_insert_row_and_set_alignment() {
5558 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
5559 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
5560 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
5562 ed.source_str().unwrap(),
5563 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
5564 );
5565 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
5567 }
5568
5569 #[test]
5570 fn editor_table_edit_off_a_table_is_not_found() {
5571 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
5572 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
5573 }
5574
5575 #[test]
5576 fn editor_set_block_converts_setext_heading() {
5577 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
5579 ed.set_block(0, BlockKind::Heading(1))
5580 .expect("setext to atx");
5581 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
5582 }
5583
5584 #[test]
5585 fn editor_unwrap_and_smart_delete() {
5586 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
5587 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
5589
5590 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
5591 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
5593 }
5594
5595 #[test]
5596 fn editor_directives_require_the_extension_flag() {
5597 let src = ":::vis{.public}\nhi\n:::\n";
5598 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
5601 assert_eq!(plain.query("directive").expect("query").len(), 0);
5602 let mut ext = Editor::new_ext(
5604 src.as_bytes(),
5605 Format::Markdown,
5606 MarkdownExtensions {
5607 directives: true,
5608 ..Default::default()
5609 },
5610 )
5611 .expect("editor");
5612 assert_eq!(ext.query("directive").expect("query").len(), 1);
5613 }
5614
5615 #[test]
5616 fn document_html_elements_make_embedded_img_queryable() {
5617 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
5618 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
5620 assert_eq!(plain.query("image").expect("query").len(), 0);
5621 let mut ext = Document::parse_str_with(
5623 src,
5624 Format::Markdown,
5625 MarkdownExtensions {
5626 html_elements: true,
5627 ..Default::default()
5628 },
5629 )
5630 .expect("parse");
5631 let images = ext.query("image").expect("query");
5632 assert_eq!(images.len(), 1);
5633 assert_eq!(images[0].kind, Kind::Image);
5634 }
5635
5636 #[test]
5637 fn editor_filter_public_audience_view() {
5638 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
5639 let mut ed = Editor::new_ext(
5640 src.as_bytes(),
5641 Format::Markdown,
5642 MarkdownExtensions {
5643 directives: true,
5644 ..Default::default()
5645 },
5646 )
5647 .expect("editor");
5648 ed.filter(
5650 "directive[name=vis]",
5651 Some("directive[class~=public]"),
5652 true,
5653 )
5654 .expect("filter");
5655 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
5656 }
5657
5658 #[test]
5659 fn editor_filter_rejects_a_malformed_selector() {
5660 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
5661 assert_eq!(
5662 ed.filter("list >", None, false),
5663 Err(Error::InvalidArgument)
5664 );
5665 }
5666
5667 #[test]
5668 fn builder_builds_and_renders_a_document() {
5669 let mut b = Builder::new().expect("builder");
5670
5671 let title = b.add_text(TextKind::Str, "Title").unwrap();
5673 let heading = b.add_heading(1).unwrap();
5674 b.set_children(heading, &[title]).unwrap();
5675
5676 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
5677 let world = b.add_text(TextKind::Str, "world").unwrap();
5678 let emph = b.add(VoidKind::Emph).unwrap();
5679 b.set_children(emph, &[world]).unwrap();
5680 let para = b.add(VoidKind::Para).unwrap();
5681 b.set_children(para, &[hello, emph]).unwrap();
5682
5683 let doc = b.add(VoidKind::Doc).unwrap();
5684 b.set_children(doc, &[heading, para]).unwrap();
5685
5686 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
5687 assert!(html.contains("<h1>Title</h1>"), "{html}");
5688 assert!(html.contains("<em>world</em>"), "{html}");
5689
5690 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5691 assert!(md.contains("# Title"), "{md}");
5692 assert!(md.contains("*world*"), "{md}");
5693
5694 let matches = b.query(doc, "heading").unwrap();
5695 assert_eq!(matches.len(), 1);
5696 assert_eq!(matches[0].kind, Kind::Heading);
5697
5698 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
5699 assert!(json.contains("\"kind\": \"doc\""), "{json}");
5700 }
5701
5702 #[test]
5703 fn builder_element_with_attributes() {
5704 let mut b = Builder::new().expect("builder");
5705 let inner = b.add_text(TextKind::Str, "hi").unwrap();
5706 let el = b.add_element("section").unwrap();
5707 b.set_children(el, &[inner]).unwrap();
5708 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)])
5709 .unwrap();
5710
5711 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
5712 assert!(html.contains("<section"), "{html}");
5713 assert!(html.contains("class=\"note\""), "{html}");
5714 assert!(html.contains("hidden"), "{html}");
5715 }
5716
5717 #[test]
5718 fn builder_lists_round_trip_to_markdown() {
5719 let mut b = Builder::new().expect("builder");
5720
5721 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
5723 let one_para = b.add(VoidKind::Para).unwrap();
5724 b.set_children(one_para, &[one_txt]).unwrap();
5725 let one = b.add(VoidKind::ListItem).unwrap();
5726 b.set_children(one, &[one_para]).unwrap();
5727
5728 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
5729 let two_para = b.add(VoidKind::Para).unwrap();
5730 b.set_children(two_para, &[two_txt]).unwrap();
5731 let two = b.add(VoidKind::ListItem).unwrap();
5732 b.set_children(two, &[two_para]).unwrap();
5733
5734 let list = b
5735 .add_ordered_list(
5736 OrderedNumbering::Decimal,
5737 OrderedDelim::Period,
5738 true,
5739 Some(1),
5740 )
5741 .unwrap();
5742 b.set_children(list, &[one, two]).unwrap();
5743 let doc = b.add(VoidKind::Doc).unwrap();
5744 b.set_children(doc, &[list]).unwrap();
5745
5746 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5747 assert!(md.contains("1. one"), "{md}");
5748 assert!(md.contains("2. two"), "{md}");
5749 }
5750
5751 #[test]
5752 fn builder_rejects_invalid_kind_and_id() {
5753 let b = Builder::new().expect("builder");
5754 let mut id = 0u32;
5758 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
5759 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5760
5761 let mut ptr = std::ptr::null();
5763 let mut len = 0usize;
5764 let status =
5765 unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
5766 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5767 }
5768
5769 fn all_gestures() -> Vec<Gesture> {
5773 let inline = [
5774 InlineKind::Strong,
5775 InlineKind::Emph,
5776 InlineKind::Verbatim,
5777 InlineKind::Mark,
5778 InlineKind::Superscript,
5779 InlineKind::Subscript,
5780 InlineKind::Insert,
5781 InlineKind::Delete,
5782 ];
5783 let mut all: Vec<Gesture> = Vec::new();
5784 for k in inline {
5785 all.push(Gesture::WrapRange(k));
5786 all.push(Gesture::ToggleInline(k));
5787 }
5788 for k in [
5789 BlockContainerKind::BlockQuote,
5790 BlockContainerKind::BulletList,
5791 BlockContainerKind::OrderedList,
5792 ] {
5793 all.push(Gesture::ToggleBlockContainer(k));
5794 }
5795 all.extend([
5796 Gesture::SetMarkColor,
5797 Gesture::SetBlock,
5798 Gesture::InsertThematicBreak,
5799 Gesture::ToggleCodeBlock,
5800 Gesture::SetCodeLanguage,
5801 Gesture::ToggleTaskItem,
5802 Gesture::SetTaskChecked,
5803 Gesture::ToggleTaskChecked,
5804 Gesture::InsertLink,
5805 Gesture::InsertImage,
5806 Gesture::InsertFootnote,
5807 Gesture::InsertLiteral,
5808 Gesture::InsertLineBreak,
5809 Gesture::SplitBlock,
5810 Gesture::RenumberOrderedLists,
5811 Gesture::TableInsertRow,
5812 Gesture::TableDeleteRow,
5813 Gesture::TableInsertColumn,
5814 Gesture::TableDeleteColumn,
5815 Gesture::TableSetAlignment,
5816 Gesture::TableMoveRow,
5817 Gesture::TableMoveColumn,
5818 ]);
5819 all
5820 }
5821
5822 #[test]
5823 fn the_wire_space_ends_where_the_sweep_does() {
5824 let mut codes: Vec<c_int> = all_gestures().iter().map(|g| g.to_c().0).collect();
5830 codes.sort_unstable();
5831 codes.dedup();
5832 assert_eq!(codes, (0..=24).collect::<Vec<c_int>>());
5833
5834 let mut supported = -1;
5835 for code in &codes {
5836 let status = unsafe {
5837 ffi::twig_format_supports(
5838 ffi::TwigFormat::from(Format::Markdown) as c_int,
5839 *code,
5840 0,
5841 &mut supported,
5842 )
5843 };
5844 assert_eq!(Error::from_status(status), Ok(()), "code {code} did not decode");
5845 }
5846 let status = unsafe {
5848 ffi::twig_format_supports(
5849 ffi::TwigFormat::from(Format::Markdown) as c_int,
5850 25,
5851 0,
5852 &mut supported,
5853 )
5854 };
5855 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5856 }
5857
5858 #[test]
5859 fn supports_answers_per_gesture_where_authorable_cannot() {
5860 assert!(Format::Html.is_authorable());
5865 assert!(Format::Html.supports(Gesture::ToggleInline(InlineKind::Strong)));
5866 assert!(Format::Html.supports(Gesture::SetBlock));
5867 assert!(Format::Html.supports(Gesture::InsertLiteral));
5868 assert!(!Format::Html.supports(Gesture::ToggleBlockContainer(
5869 BlockContainerKind::BlockQuote
5870 )));
5871 assert!(!Format::Html.supports(Gesture::ToggleCodeBlock));
5872 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5876 assert!(!Format::Html.supports(Gesture::TableSetAlignment));
5877 assert!(!Format::Html.supports(Gesture::SplitBlock));
5878 assert!(!Format::Html.supports(Gesture::RenumberOrderedLists));
5879 assert!(Format::Markdown.supports(Gesture::TableInsertRow));
5880 assert!(Format::Djot.supports(Gesture::SplitBlock));
5881
5882 for fmt in [Format::Xml] {
5885 assert!(!fmt.is_authorable());
5886 for g in all_gestures() {
5887 assert!(!fmt.supports(g), "{fmt:?} claims to spell {g:?}");
5888 }
5889 }
5890 assert!(Format::Asciidoc.is_authorable());
5893 assert!(Format::Asciidoc.supports(Gesture::SetBlock));
5894 assert!(Format::Asciidoc.supports(Gesture::ToggleInline(InlineKind::Mark)));
5895 assert!(!Format::Asciidoc.supports(Gesture::InsertLink));
5896 assert!(!Format::Asciidoc.supports(Gesture::TableInsertRow));
5897
5898 assert!(Format::Djot.supports(Gesture::ToggleInline(InlineKind::Mark)));
5901 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
5902 assert!(Format::Markdown.supports(Gesture::InsertLineBreak));
5903 assert!(!Format::Djot.supports(Gesture::InsertLineBreak));
5904 }
5905
5906 #[test]
5907 fn supports_agrees_with_what_the_editor_then_does() {
5908 for fmt in [Format::Djot, Format::Markdown, Format::Html] {
5913 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5914 let claimed = fmt.supports(Gesture::ToggleInline(InlineKind::Mark));
5915 let observed = ed.toggle_inline(0, 2, InlineKind::Mark);
5916 assert_eq!(
5917 claimed,
5918 !matches!(observed, Err(Error::UnsupportedFormat)),
5919 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5920 );
5921
5922 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5923 let claimed = fmt.supports(Gesture::SetBlock);
5924 let observed = ed.set_block(0, BlockKind::Heading(1));
5925 assert_eq!(
5926 claimed,
5927 !matches!(observed, Err(Error::UnsupportedFormat)),
5928 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5929 );
5930 }
5931
5932 let src = "<table><tr><td>a</td></tr></table>";
5936 let mut ed = Editor::new_str(src, Format::Html).expect("editor");
5937 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5938 assert_eq!(ed.table_insert_row(15, true), Err(Error::UnsupportedFormat));
5939 assert_eq!(ed.renumber_ordered_lists(15), Err(Error::UnsupportedFormat));
5940 assert!(matches!(ed.split_block(15), Err(Error::UnsupportedFormat)));
5941 assert_eq!(ed.source().expect("source"), src.as_bytes());
5942 }
5943
5944 #[test]
5945 fn supports_rides_the_gestures_own_kind_space() {
5946 let (g, k) = Gesture::ToggleBlockContainer(BlockContainerKind::BulletList).to_c();
5951 assert_eq!((g, k), (3, 1));
5952 let (g, k) = Gesture::ToggleInline(InlineKind::Emph).to_c();
5953 assert_eq!((g, k), (1, 1));
5954 assert_eq!(Gesture::InsertLink.to_c(), (10, 0));
5957
5958 let mut out: c_int = 0;
5960 let status = unsafe {
5961 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 10, 3, &mut out)
5962 };
5963 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5964 let status = unsafe {
5965 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 9999, 0, &mut out)
5966 };
5967 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5968 }
5969}