1mod error;
2
3pub(crate) use twig_sys as ffi;
8
9use std::marker::PhantomData;
10use std::ops::Range;
11use std::os::raw::{c_char, c_int};
12use std::ptr::NonNull;
13
14pub use error::Error;
15pub use ffi::TwigSpan as Span;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25#[non_exhaustive]
26pub enum Format {
27 Djot,
28 Markdown,
29 Xml,
30 Html,
31 Asciidoc,
41}
42
43impl From<Format> for ffi::TwigFormat {
44 fn from(value: Format) -> Self {
45 match value {
46 Format::Djot => ffi::TwigFormat::Djot,
47 Format::Markdown => ffi::TwigFormat::Markdown,
48 Format::Xml => ffi::TwigFormat::Xml,
49 Format::Html => ffi::TwigFormat::Html,
50 Format::Asciidoc => ffi::TwigFormat::Asciidoc,
51 }
52 }
53}
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70#[non_exhaustive]
71pub enum Target {
72 Djot,
73 Markdown,
74 Xml,
75 Html,
76 Asciidoc,
82}
83
84impl Target {
85 pub fn as_format(self) -> Option<Format> {
92 match self {
93 Target::Djot => Some(Format::Djot),
94 Target::Markdown => Some(Format::Markdown),
95 Target::Xml => Some(Format::Xml),
96 Target::Html => Some(Format::Html),
97 Target::Asciidoc => Some(Format::Asciidoc),
98 }
99 }
100}
101
102impl From<Format> for Target {
106 fn from(value: Format) -> Self {
107 match value {
108 Format::Djot => Target::Djot,
109 Format::Markdown => Target::Markdown,
110 Format::Xml => Target::Xml,
111 Format::Html => Target::Html,
112 Format::Asciidoc => Target::Asciidoc,
113 }
114 }
115}
116
117impl From<Target> for ffi::TwigFormat {
118 fn from(value: Target) -> Self {
119 match value {
120 Target::Djot => ffi::TwigFormat::Djot,
121 Target::Markdown => ffi::TwigFormat::Markdown,
122 Target::Xml => ffi::TwigFormat::Xml,
123 Target::Html => ffi::TwigFormat::Html,
124 Target::Asciidoc => ffi::TwigFormat::Asciidoc,
125 }
126 }
127}
128
129#[derive(Clone, Debug, Eq, PartialEq, Hash)]
163#[non_exhaustive]
164pub enum Kind {
165 Doc,
167 Para,
169 Heading,
170 ThematicBreak,
171 Section,
172 CodeBlock,
173 RawBlock,
174 Metadata,
175 BlockQuote,
176 BulletList,
177 OrderedList,
178 TaskList,
179 DefinitionList,
180 LineBlock,
181 Table,
182 ListItem,
184 TaskListItem,
185 DefinitionListItem,
186 Term,
187 Definition,
188 Line,
189 Row,
190 Cell,
191 Column,
192 Caption,
193 Footnote,
194 Reference,
195 Citation,
196 Substitution,
197 Str,
199 SoftBreak,
200 HardBreak,
201 NonBreakingSpace,
202 RawInline,
203 SmartPunctuation,
204 Link,
205 Image,
206 Emph,
208 Strong,
209 Mark,
210 Superscript,
211 Subscript,
212 Insert,
213 Delete,
214 DoubleQuoted,
215 SingleQuoted,
216 Symb,
218 Verbatim,
219 InlineMath,
220 DisplayMath,
221 Url,
222 Email,
223 FootnoteReference,
224 CitationReference,
225 SubstitutionReference,
226 Container,
228 ProcessingInstruction,
229 Comment,
230 Doctype,
231 Cdata,
232 Other(String),
239}
240
241impl Kind {
242 pub fn as_str(&self) -> &str {
245 match self {
246 Kind::Doc => "doc",
247 Kind::Para => "para",
248 Kind::Heading => "heading",
249 Kind::ThematicBreak => "thematic_break",
250 Kind::Section => "section",
251 Kind::CodeBlock => "code_block",
252 Kind::RawBlock => "raw_block",
253 Kind::Metadata => "metadata",
254 Kind::BlockQuote => "block_quote",
255 Kind::BulletList => "bullet_list",
256 Kind::OrderedList => "ordered_list",
257 Kind::TaskList => "task_list",
258 Kind::DefinitionList => "definition_list",
259 Kind::LineBlock => "line_block",
260 Kind::Table => "table",
261 Kind::ListItem => "list_item",
262 Kind::TaskListItem => "task_list_item",
263 Kind::DefinitionListItem => "definition_list_item",
264 Kind::Term => "term",
265 Kind::Definition => "definition",
266 Kind::Line => "line",
267 Kind::Row => "row",
268 Kind::Cell => "cell",
269 Kind::Column => "column",
270 Kind::Caption => "caption",
271 Kind::Footnote => "footnote",
272 Kind::Reference => "reference",
273 Kind::Citation => "citation",
274 Kind::Substitution => "substitution",
275 Kind::Str => "str",
276 Kind::SoftBreak => "soft_break",
277 Kind::HardBreak => "hard_break",
278 Kind::NonBreakingSpace => "non_breaking_space",
279 Kind::RawInline => "raw_inline",
280 Kind::SmartPunctuation => "smart_punctuation",
281 Kind::Link => "link",
282 Kind::Image => "image",
283 Kind::Container => "container",
284 Kind::ProcessingInstruction => "processing_instruction",
285 Kind::Emph => "emph",
286 Kind::Strong => "strong",
287 Kind::Mark => "mark",
288 Kind::Superscript => "superscript",
289 Kind::Subscript => "subscript",
290 Kind::Insert => "insert",
291 Kind::Delete => "delete",
292 Kind::DoubleQuoted => "double_quoted",
293 Kind::SingleQuoted => "single_quoted",
294 Kind::Symb => "symb",
295 Kind::Verbatim => "verbatim",
296 Kind::InlineMath => "inline_math",
297 Kind::DisplayMath => "display_math",
298 Kind::Url => "url",
299 Kind::Email => "email",
300 Kind::FootnoteReference => "footnote_reference",
301 Kind::CitationReference => "citation_reference",
302 Kind::SubstitutionReference => "substitution_reference",
303 Kind::Comment => "comment",
304 Kind::Doctype => "doctype",
305 Kind::Cdata => "cdata",
306 Kind::Other(name) => name.as_str(),
307 }
308 }
309
310 pub fn is_unknown(&self) -> bool {
314 matches!(self, Kind::Other(_))
315 }
316}
317
318impl From<&str> for Kind {
319 fn from(name: &str) -> Self {
320 match name {
321 "doc" => Kind::Doc,
322 "para" => Kind::Para,
323 "heading" => Kind::Heading,
324 "thematic_break" => Kind::ThematicBreak,
325 "section" => Kind::Section,
326 "code_block" => Kind::CodeBlock,
327 "raw_block" => Kind::RawBlock,
328 "metadata" => Kind::Metadata,
329 "block_quote" => Kind::BlockQuote,
330 "bullet_list" => Kind::BulletList,
331 "ordered_list" => Kind::OrderedList,
332 "task_list" => Kind::TaskList,
333 "definition_list" => Kind::DefinitionList,
334 "line_block" => Kind::LineBlock,
335 "table" => Kind::Table,
336 "list_item" => Kind::ListItem,
337 "task_list_item" => Kind::TaskListItem,
338 "definition_list_item" => Kind::DefinitionListItem,
339 "term" => Kind::Term,
340 "definition" => Kind::Definition,
341 "line" => Kind::Line,
342 "row" => Kind::Row,
343 "cell" => Kind::Cell,
344 "column" => Kind::Column,
345 "caption" => Kind::Caption,
346 "footnote" => Kind::Footnote,
347 "reference" => Kind::Reference,
348 "citation" => Kind::Citation,
349 "substitution" => Kind::Substitution,
350 "str" => Kind::Str,
351 "soft_break" => Kind::SoftBreak,
352 "hard_break" => Kind::HardBreak,
353 "non_breaking_space" => Kind::NonBreakingSpace,
354 "raw_inline" => Kind::RawInline,
355 "smart_punctuation" => Kind::SmartPunctuation,
356 "link" => Kind::Link,
357 "image" => Kind::Image,
358 "container" => Kind::Container,
359 "processing_instruction" => Kind::ProcessingInstruction,
360 "emph" => Kind::Emph,
361 "strong" => Kind::Strong,
362 "mark" => Kind::Mark,
363 "superscript" => Kind::Superscript,
364 "subscript" => Kind::Subscript,
365 "insert" => Kind::Insert,
366 "delete" => Kind::Delete,
367 "double_quoted" => Kind::DoubleQuoted,
368 "single_quoted" => Kind::SingleQuoted,
369 "symb" => Kind::Symb,
370 "verbatim" => Kind::Verbatim,
371 "inline_math" => Kind::InlineMath,
372 "display_math" => Kind::DisplayMath,
373 "url" => Kind::Url,
374 "email" => Kind::Email,
375 "footnote_reference" => Kind::FootnoteReference,
376 "citation_reference" => Kind::CitationReference,
377 "substitution_reference" => Kind::SubstitutionReference,
378 "comment" => Kind::Comment,
379 "doctype" => Kind::Doctype,
380 "cdata" => Kind::Cdata,
381 other => Kind::Other(other.to_string()),
382 }
383 }
384}
385
386impl std::fmt::Display for Kind {
387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 f.write_str(self.as_str())
389 }
390}
391
392#[derive(Clone, Debug, Eq, PartialEq)]
394pub struct QueryMatch {
395 pub node_id: u32,
397 pub span: Range<usize>,
399 pub content_span: Option<Range<usize>>,
402 pub kind: Kind,
405}
406
407#[derive(Clone, Debug, Eq, PartialEq)]
414pub struct Change {
415 pub old: Range<usize>,
416 pub new: Range<usize>,
417}
418
419impl Change {
420 pub fn delta(&self) -> isize {
422 self.new.len() as isize - self.old.len() as isize
423 }
424
425 fn from_ffi(c: ffi::TwigChange) -> Self {
426 Change {
427 old: c.old_span.start..c.old_span.end,
428 new: c.new_span.start..c.new_span.end,
429 }
430 }
431}
432
433#[derive(Clone, Debug, Eq, PartialEq)]
444#[non_exhaustive]
445pub struct FlatNode {
446 pub id: NodeId,
447 pub parent: Option<NodeId>,
448 pub first_child: Option<NodeId>,
449 pub next_sibling: Option<NodeId>,
450 pub span: Range<usize>,
451 pub content_span: Option<Range<usize>>,
452 pub level: Option<u32>,
454 pub kind: Kind,
455 pub text: Option<String>,
456 pub destination: Option<String>,
457 pub head: Option<bool>,
460 pub alignment: Option<Alignment>,
466 pub name: Option<String>,
478 pub directive_form: Option<DirectiveForm>,
493 pub origin: Option<ContainerOrigin>,
503 pub marker_span: Option<Range<usize>>,
522 pub checked: Option<bool>,
535 pub attrs: Vec<(String, Option<String>)>,
539}
540
541#[derive(Clone, Debug, Default, Eq, PartialEq)]
549pub struct LinePrefix {
550 pub text: String,
552 pub columns: usize,
554}
555
556#[derive(Clone, Copy, Debug, Eq, PartialEq)]
561pub enum InlineKind {
562 Strong,
563 Emph,
564 Verbatim,
565 Mark,
566 Superscript,
567 Subscript,
568 Insert,
569 Delete,
570}
571
572impl InlineKind {
573 fn to_c(self) -> c_int {
574 match self {
575 InlineKind::Strong => 0,
576 InlineKind::Emph => 1,
577 InlineKind::Verbatim => 2,
578 InlineKind::Mark => 3,
579 InlineKind::Superscript => 4,
580 InlineKind::Subscript => 5,
581 InlineKind::Insert => 6,
582 InlineKind::Delete => 7,
583 }
584 }
585}
586
587#[derive(Clone, Copy, Debug, Eq, PartialEq)]
589pub enum BlockKind {
590 Paragraph,
591 Heading(u32),
593}
594
595impl BlockKind {
596 fn to_c(self) -> (c_int, u32) {
598 match self {
599 BlockKind::Paragraph => (0, 0),
600 BlockKind::Heading(level) => (1, level),
601 }
602 }
603}
604
605#[derive(Clone, Copy, Debug, Eq, PartialEq)]
611pub enum BlockContainerKind {
612 BlockQuote,
613 BulletList,
614 OrderedList,
615}
616
617impl BlockContainerKind {
618 fn to_c(self) -> c_int {
619 match self {
620 BlockContainerKind::BlockQuote => 0,
621 BlockContainerKind::BulletList => 1,
622 BlockContainerKind::OrderedList => 2,
623 }
624 }
625}
626
627#[derive(Clone, Copy, Debug, Eq, PartialEq)]
660#[non_exhaustive]
661pub enum Gesture {
662 WrapRange(InlineKind),
663 ToggleInline(InlineKind),
664 SetBlock,
665 ToggleBlockContainer(BlockContainerKind),
666 InsertThematicBreak,
667 ToggleCodeBlock,
668 SetCodeLanguage,
669 ToggleTaskItem,
670 SetTaskChecked,
671 ToggleTaskChecked,
672 InsertLink,
673 InsertImage,
674 InsertFootnote,
675 InsertLiteral,
676 InsertLineBreak,
677}
678
679impl Gesture {
680 fn to_c(self) -> (c_int, c_int) {
685 match self {
686 Gesture::WrapRange(k) => (0, k.to_c()),
687 Gesture::ToggleInline(k) => (1, k.to_c()),
688 Gesture::SetBlock => (2, 0),
689 Gesture::ToggleBlockContainer(k) => (3, k.to_c()),
690 Gesture::InsertThematicBreak => (4, 0),
691 Gesture::ToggleCodeBlock => (5, 0),
692 Gesture::SetCodeLanguage => (6, 0),
693 Gesture::ToggleTaskItem => (7, 0),
694 Gesture::SetTaskChecked => (8, 0),
695 Gesture::ToggleTaskChecked => (9, 0),
696 Gesture::InsertLink => (10, 0),
697 Gesture::InsertImage => (11, 0),
698 Gesture::InsertFootnote => (12, 0),
699 Gesture::InsertLiteral => (13, 0),
700 Gesture::InsertLineBreak => (14, 0),
701 }
702 }
703}
704
705impl Format {
706 pub fn supports(self, gesture: Gesture) -> bool {
731 let (g, k) = gesture.to_c();
732 let mut supported: c_int = 0;
733 let status = unsafe {
734 ffi::twig_format_supports(ffi::TwigFormat::from(self) as c_int, g, k, &mut supported)
735 };
736 debug_assert!(
737 Error::from_status(status).is_ok(),
738 "twig_format_supports rejected a combination the Rust types make unrepresentable",
739 );
740 supported == 1
741 }
742
743 pub fn is_authorable(self) -> bool {
755 let mut authorable: c_int = 0;
756 let status = unsafe {
757 ffi::twig_format_is_authorable(ffi::TwigFormat::from(self) as c_int, &mut authorable)
758 };
759 debug_assert!(Error::from_status(status).is_ok(), "unknown format code");
760 authorable == 1
761 }
762}
763
764#[derive(Clone, Copy, Debug, Eq, PartialEq)]
765pub struct Version {
766 pub major: u8,
767 pub minor: u8,
768 pub patch: u8,
769}
770
771pub fn version() -> Version {
772 let packed = unsafe { ffi::twig_version() };
773 Version {
774 major: (packed >> 16) as u8,
775 minor: (packed >> 8) as u8,
776 patch: packed as u8,
777 }
778}
779
780pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
786
787pub fn abi_version() -> u32 {
793 unsafe { ffi::twig_abi_version() }
794}
795
796pub fn version_string() -> &'static str {
797 let ptr = unsafe { ffi::twig_version_string() };
798 unsafe { std::ffi::CStr::from_ptr(ptr) }
799 .to_str()
800 .unwrap_or("")
801}
802
803#[derive(Debug)]
804pub struct Document {
805 raw: NonNull<ffi::TwigDocument>,
806}
807
808impl Document {
809 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
810 Self::parse_with(input, format, MarkdownExtensions::default())
811 }
812
813 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
814 Self::parse(input.as_bytes(), format)
815 }
816
817 pub fn parse_with(
823 input: &[u8],
824 format: Format,
825 extensions: MarkdownExtensions,
826 ) -> Result<Self, Error> {
827 let mut raw = std::ptr::null_mut();
828 let ffi_format: ffi::TwigFormat = format.into();
829 let status = unsafe {
830 ffi::twig_parse_ext(
831 input.as_ptr(),
832 input.len(),
833 ffi_format as i32,
834 extensions.to_flags(),
835 &mut raw,
836 )
837 };
838 Error::from_status(status)?;
839 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
840 Ok(Self { raw })
841 }
842
843 pub fn parse_str_with(
845 input: &str,
846 format: Format,
847 extensions: MarkdownExtensions,
848 ) -> Result<Self, Error> {
849 Self::parse_with(input.as_bytes(), format, extensions)
850 }
851
852 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
855 let raw = self.raw.as_ptr();
856 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
857 }
858
859 pub fn serialize_to(&mut self, target: Target) -> Result<Vec<u8>, Error> {
870 let raw = self.raw.as_ptr();
871 let ffi_target: ffi::TwigFormat = target.into();
872 collect_bytes(|ptr, len| unsafe {
873 ffi::twig_document_serialize(raw, ffi_target as i32, ptr, len)
874 })
875 }
876
877 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
884 self.serialize_to(format.into())
885 }
886
887 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
890 let raw = self.raw.as_ptr();
891 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
892 }
893
894 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
903 let raw = self.raw.as_ptr();
904 collect_matches(|ptr, len| unsafe {
905 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
906 })
907 }
908
909 pub fn span(&mut self, node: NodeId) -> Result<Range<usize>, Error> {
911 let mut span = ffi::TwigSpan { start: 0, end: 0 };
912 let status = unsafe { ffi::twig_document_node_span(self.raw.as_ptr(), node.0, &mut span) };
913 Error::from_status(status)?;
914 Ok(span.start..span.end)
915 }
916
917 pub fn content_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
920 let mut span = ffi::TwigSpan { start: 0, end: 0 };
921 let status =
922 unsafe { ffi::twig_document_node_content_span(self.raw.as_ptr(), node.0, &mut span) };
923 match status.0 {
924 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
925 ffi::TwigStatus::NOT_FOUND => Ok(None),
926 _ => Err(Error::from_status(status).unwrap_err()),
927 }
928 }
929
930 pub fn marker_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
934 let mut span = ffi::TwigSpan { start: 0, end: 0 };
935 let status =
936 unsafe { ffi::twig_document_node_marker_span(self.raw.as_ptr(), node.0, &mut span) };
937 match status.0 {
938 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
939 ffi::TwigStatus::NOT_FOUND => Ok(None),
940 _ => Err(Error::from_status(status).unwrap_err()),
941 }
942 }
943
944 pub fn attrs_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
959 let mut span = ffi::TwigSpan { start: 0, end: 0 };
960 let status =
961 unsafe { ffi::twig_document_attrs_span(self.raw.as_ptr(), node.0, &mut span) };
962 match status.0 {
963 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
964 ffi::TwigStatus::NOT_FOUND => Ok(None),
965 _ => Err(Error::from_status(status).unwrap_err()),
966 }
967 }
968
969 pub fn line_prefix(&mut self, offset: usize) -> Result<Option<Range<usize>>, Error> {
986 let mut span = ffi::TwigSpan { start: 0, end: 0 };
987 let status =
988 unsafe { ffi::twig_document_line_prefix(self.raw.as_ptr(), offset, &mut span) };
989 match status.0 {
990 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
991 ffi::TwigStatus::NOT_FOUND => Ok(None),
992 _ => Err(Error::from_status(status).unwrap_err()),
993 }
994 }
995
996 pub fn continuation_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1022 self.prefix_via(offset, ffi::twig_document_continuation_prefix)
1023 }
1024
1025 pub fn blank_line_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1037 self.prefix_via(offset, ffi::twig_document_blank_line_prefix)
1038 }
1039
1040 fn prefix_via(
1042 &mut self,
1043 offset: usize,
1044 f: unsafe extern "C" fn(
1045 *mut ffi::TwigDocument,
1046 usize,
1047 *mut *const u8,
1048 *mut usize,
1049 *mut usize,
1050 ) -> ffi::TwigStatus,
1051 ) -> Result<LinePrefix, Error> {
1052 let mut ptr: *const u8 = std::ptr::null();
1053 let mut len = 0usize;
1054 let mut columns = 0usize;
1055 let status = unsafe { f(self.raw.as_ptr(), offset, &mut ptr, &mut len, &mut columns) };
1056 Error::from_status(status)?;
1057 let text = if ptr.is_null() || len == 0 {
1058 String::new()
1059 } else {
1060 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1061 String::from_utf8(bytes.to_vec()).map_err(|_| Error::Internal)?
1062 };
1063 Ok(LinePrefix { text, columns })
1064 }
1065
1066 pub fn cell_extent(&mut self, node: NodeId) -> Result<Option<(u32, u32)>, Error> {
1078 let raw = self.raw.as_ptr();
1079 let mut colspan: u32 = 0;
1080 let status = unsafe { ffi::twig_document_cell_colspan(raw, node.0, &mut colspan) };
1081 match status.0 {
1082 ffi::TwigStatus::OK => {}
1083 ffi::TwigStatus::NOT_FOUND => return Ok(None),
1084 _ => return Err(Error::from_status(status).unwrap_err()),
1085 }
1086 let mut rowspan: u32 = 0;
1087 Error::from_status(unsafe { ffi::twig_document_cell_rowspan(raw, node.0, &mut rowspan) })?;
1088 Ok(Some((colspan, rowspan)))
1089 }
1090
1091 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1096 let raw = self.raw.as_ptr();
1097 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_nodes(raw, ptr, len) })
1098 }
1099
1100 pub fn definitions(&mut self) -> Result<Vec<QueryMatch>, Error> {
1117 let raw = self.raw.as_ptr();
1118 collect_matches(|ptr, len| unsafe { ffi::twig_document_definitions(raw, ptr, len) })
1119 }
1120
1121 pub fn diagnostics(&mut self, target: Target) -> Result<Vec<Warning>, Error> {
1141 let raw = self.raw.as_ptr();
1142 let code = ffi::TwigFormat::from(target) as c_int;
1143 let mut ptr: *const ffi::TwigWarning = std::ptr::null();
1144 let mut len = 0usize;
1145 let status = unsafe { ffi::twig_document_diagnostics(raw, code, &mut ptr, &mut len) };
1146 Error::from_status(status)?;
1147 if len == 0 || ptr.is_null() {
1148 return Ok(Vec::new());
1149 }
1150 let raw_warnings = unsafe { std::slice::from_raw_parts(ptr, len) };
1151 Ok(raw_warnings
1152 .iter()
1153 .map(|w| Warning {
1154 fidelity: Fidelity::from_c(w.fidelity),
1155 path: borrowed_bytes(w.path_ptr, w.path_len).unwrap_or_default(),
1156 kind: Kind::from(borrowed_cstr(w.kind).unwrap_or_default().as_str()),
1157 })
1158 .collect())
1159 }
1160
1161 pub fn children(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1167 let raw = self.raw.as_ptr();
1168 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1169 collect_matches(|ptr, len| unsafe { ffi::twig_document_children(raw, id, ptr, len) })
1170 }
1171
1172 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1178 let raw = self.raw.as_ptr();
1179 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_subtree(raw, node.0, ptr, len) })
1180 }
1181
1182 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1187 let mut m = empty_ffi_match();
1188 let status = unsafe { ffi::twig_document_node_at(self.raw.as_ptr(), offset, &mut m) };
1189 match status.0 {
1190 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1191 ffi::TwigStatus::NOT_FOUND => Ok(None),
1192 _ => Err(Error::from_status(status).unwrap_err()),
1193 }
1194 }
1195
1196 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1200 let raw = self.raw.as_ptr();
1201 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1202 let mut len = 0usize;
1203 let status = unsafe { ffi::twig_document_nodes_at(raw, offset, &mut ptr, &mut len) };
1204 match status.0 {
1205 ffi::TwigStatus::OK => {}
1206 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1207 _ => return Err(Error::from_status(status).unwrap_err()),
1208 }
1209 if len == 0 || ptr.is_null() {
1210 return Ok(Vec::new());
1211 }
1212 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1213 raw_matches.iter().map(query_match_from_ffi).collect()
1214 }
1215
1216 pub fn node_at_caret(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1239 let mut m = empty_ffi_match();
1240 let status = unsafe { ffi::twig_document_node_at_caret(self.raw.as_ptr(), offset, &mut m) };
1241 match status.0 {
1242 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1243 ffi::TwigStatus::NOT_FOUND => Ok(None),
1244 _ => Err(Error::from_status(status).unwrap_err()),
1245 }
1246 }
1247
1248 pub fn ancestors_at_caret(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1252 let raw = self.raw.as_ptr();
1253 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1254 let mut len = 0usize;
1255 let status = unsafe { ffi::twig_document_nodes_at_caret(raw, offset, &mut ptr, &mut len) };
1256 match status.0 {
1257 ffi::TwigStatus::OK => {}
1258 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1259 _ => return Err(Error::from_status(status).unwrap_err()),
1260 }
1261 if len == 0 || ptr.is_null() {
1262 return Ok(Vec::new());
1263 }
1264 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1265 raw_matches.iter().map(query_match_from_ffi).collect()
1266 }
1267}
1268
1269#[derive(Debug)]
1280pub struct DocumentView<'a> {
1281 doc: Document,
1282 _editor: PhantomData<&'a mut Editor>,
1283}
1284
1285impl std::ops::Deref for DocumentView<'_> {
1286 type Target = Document;
1287
1288 fn deref(&self) -> &Document {
1289 &self.doc
1290 }
1291}
1292
1293impl std::ops::DerefMut for DocumentView<'_> {
1294 fn deref_mut(&mut self) -> &mut Document {
1295 &mut self.doc
1296 }
1297}
1298
1299impl Drop for Document {
1300 fn drop(&mut self) {
1301 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
1302 }
1303}
1304
1305#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1311pub struct MarkdownExtensions {
1312 pub directives: bool,
1314 pub math: bool,
1316 pub html_elements: bool,
1321}
1322
1323impl MarkdownExtensions {
1324 fn to_flags(self) -> u32 {
1325 let mut flags = 0;
1326 if self.directives {
1327 flags |= ffi::TWIG_MD_DIRECTIVES;
1328 }
1329 if self.math {
1330 flags |= ffi::TWIG_MD_MATH;
1331 }
1332 if self.html_elements {
1333 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
1334 }
1335 flags
1336 }
1337}
1338
1339#[derive(Debug)]
1345pub struct Editor {
1346 raw: NonNull<ffi::TwigEditor>,
1347}
1348
1349impl Editor {
1350 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
1353 let mut raw = std::ptr::null_mut();
1354 let ffi_format: ffi::TwigFormat = format.into();
1355 let status = unsafe {
1356 ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw)
1357 };
1358 Error::from_status(status)?;
1359 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1360 Ok(Self { raw })
1361 }
1362
1363 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
1364 Self::new(input.as_bytes(), format)
1365 }
1366
1367 pub fn new_ext(
1372 input: &[u8],
1373 format: Format,
1374 extensions: MarkdownExtensions,
1375 ) -> Result<Self, Error> {
1376 let mut raw = std::ptr::null_mut();
1377 let ffi_format: ffi::TwigFormat = format.into();
1378 let status = unsafe {
1379 ffi::twig_editor_create_ext(
1380 input.as_ptr(),
1381 input.len(),
1382 ffi_format as i32,
1383 extensions.to_flags(),
1384 &mut raw,
1385 )
1386 };
1387 Error::from_status(status)?;
1388 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1389 Ok(Self { raw })
1390 }
1391
1392 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1394 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1395 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
1396 })
1397 }
1398
1399 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1402 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1403 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
1404 })
1405 }
1406
1407 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1409 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1410 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
1411 })
1412 }
1413
1414 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1416 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1417 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
1418 })
1419 }
1420
1421 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
1424 let status = unsafe {
1425 ffi::twig_editor_insert_child(
1426 self.raw.as_ptr(),
1427 locator.as_ptr(),
1428 locator.len(),
1429 index,
1430 text.as_ptr(),
1431 text.len(),
1432 )
1433 };
1434 Error::from_status(status)
1435 }
1436
1437 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
1440 let status =
1441 unsafe { ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1442 Error::from_status(status)
1443 }
1444
1445 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
1448 let status = unsafe {
1449 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
1450 };
1451 Error::from_status(status)
1452 }
1453
1454 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
1458 let status =
1459 unsafe { ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1460 Error::from_status(status)
1461 }
1462
1463 pub fn filter(
1468 &mut self,
1469 drop: &str,
1470 keep: Option<&str>,
1471 unwrap_kept: bool,
1472 ) -> Result<(), Error> {
1473 let (keep_ptr, keep_len) = match keep {
1474 Some(k) => (k.as_ptr(), k.len()),
1475 None => (std::ptr::null(), 0),
1476 };
1477 let status = unsafe {
1478 ffi::twig_editor_filter(
1479 self.raw.as_ptr(),
1480 drop.as_ptr(),
1481 drop.len(),
1482 keep_ptr,
1483 keep_len,
1484 unwrap_kept as i32,
1485 )
1486 };
1487 Error::from_status(status)
1488 }
1489
1490 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
1492 let raw = self.raw.as_ptr();
1493 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
1494 }
1495
1496 pub fn source_str(&mut self) -> Result<String, Error> {
1498 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
1499 }
1500
1501 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
1504 let raw = self.raw.as_ptr();
1505 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
1506 }
1507
1508 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1511 let raw = self.raw.as_ptr();
1512 collect_matches(|ptr, len| unsafe {
1513 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
1514 })
1515 }
1516
1517 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
1527 let mut change = ffi::TwigChange {
1528 old_span: ffi::TwigSpan { start: 0, end: 0 },
1529 new_span: ffi::TwigSpan { start: 0, end: 0 },
1530 };
1531 let status = unsafe {
1532 ffi::twig_editor_edit_range(
1533 self.raw.as_ptr(),
1534 start,
1535 end,
1536 text.as_ptr(),
1537 text.len(),
1538 &mut change,
1539 )
1540 };
1541 Error::from_status(status)?;
1542 Ok(Change::from_ffi(change))
1543 }
1544
1545 pub fn last_change(&mut self) -> Option<Change> {
1551 let mut change = ffi::TwigChange {
1552 old_span: ffi::TwigSpan { start: 0, end: 0 },
1553 new_span: ffi::TwigSpan { start: 0, end: 0 },
1554 };
1555 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
1556 match status.0 {
1557 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
1558 _ => None,
1559 }
1560 }
1561
1562 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
1567 let mut change = ffi::TwigChange {
1568 old_span: ffi::TwigSpan { start: 0, end: 0 },
1569 new_span: ffi::TwigSpan { start: 0, end: 0 },
1570 };
1571 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
1572 if status.0 == ffi::TwigStatus::NOT_FOUND {
1573 return Ok(None);
1574 }
1575 Error::from_status(status)?;
1576 Ok(Some(Change::from_ffi(change)))
1577 }
1578
1579 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
1583 let mut change = ffi::TwigChange {
1584 old_span: ffi::TwigSpan { start: 0, end: 0 },
1585 new_span: ffi::TwigSpan { start: 0, end: 0 },
1586 };
1587 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
1588 if status.0 == ffi::TwigStatus::NOT_FOUND {
1589 return Ok(None);
1590 }
1591 Error::from_status(status)?;
1592 Ok(Some(Change::from_ffi(change)))
1593 }
1594
1595 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
1600 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
1601 Error::from_status(status)
1602 }
1603
1604 pub fn revision(&mut self) -> u64 {
1610 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
1611 }
1612
1613 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
1634 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1635 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
1636 match status.0 {
1637 ffi::TwigStatus::OK => Some(span.start..span.end),
1638 _ => None,
1639 }
1640 }
1641
1642 pub fn clear_dirty(&mut self) {
1647 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
1648 }
1649
1650 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
1658 let status = unsafe {
1659 ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len())
1660 };
1661 Error::from_status(status)
1662 }
1663
1664 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
1669 let raw = self.raw.as_ptr();
1670 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
1671 }
1672
1673 pub fn document(&mut self) -> Result<DocumentView<'_>, Error> {
1682 let mut raw = std::ptr::null_mut();
1683 let status = unsafe { ffi::twig_editor_document(self.raw.as_ptr(), &mut raw) };
1684 Error::from_status(status)?;
1685 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1686 Ok(DocumentView {
1687 doc: Document { raw },
1688 _editor: PhantomData,
1689 })
1690 }
1691
1692 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1697 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1698 let mut len = 0usize;
1699 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
1700 Error::from_status(status)?;
1701 if len == 0 {
1702 return Ok(Vec::new());
1703 }
1704 if ptr.is_null() {
1705 return Err(Error::Internal);
1706 }
1707 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1708 raw.iter().map(flat_node_from_ffi).collect()
1709 }
1710
1711 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1718 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1719 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1720 let mut len = 0usize;
1721 let status =
1722 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
1723 Error::from_status(status)?;
1724 if len == 0 || ptr.is_null() {
1725 return Ok(Vec::new());
1726 }
1727 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1728 raw.iter().map(query_match_from_ffi).collect()
1729 }
1730
1731 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1739 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1740 let mut len = 0usize;
1741 let status =
1742 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
1743 Error::from_status(status)?;
1744 if len == 0 || ptr.is_null() {
1745 return Ok(Vec::new());
1746 }
1747 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1748 raw.iter().map(flat_node_from_ffi).collect()
1749 }
1750
1751 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1756 let mut m = ffi::TwigQueryMatch {
1757 node_id: 0,
1758 span: ffi::TwigSpan { start: 0, end: 0 },
1759 content_span: ffi::TwigSpan { start: 0, end: 0 },
1760 has_content_span: 0,
1761 kind: std::ptr::null(),
1762 };
1763 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
1764 match status.0 {
1765 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1766 ffi::TwigStatus::NOT_FOUND => Ok(None),
1767 _ => Err(Error::from_status(status).unwrap_err()),
1768 }
1769 }
1770
1771 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1775 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1776 let mut len = 0usize;
1777 let status =
1778 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
1779 match status.0 {
1780 ffi::TwigStatus::OK => {}
1781 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1782 _ => return Err(Error::from_status(status).unwrap_err()),
1783 }
1784 if len == 0 || ptr.is_null() {
1785 return Ok(Vec::new());
1786 }
1787 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1788 raw.iter().map(query_match_from_ffi).collect()
1789 }
1790
1791 pub fn wrap_range(
1799 &mut self,
1800 start: usize,
1801 end: usize,
1802 kind: InlineKind,
1803 ) -> Result<Change, Error> {
1804 self.change_op(|ed, out| unsafe {
1805 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
1806 })
1807 }
1808
1809 pub fn toggle_inline(
1814 &mut self,
1815 start: usize,
1816 end: usize,
1817 kind: InlineKind,
1818 ) -> Result<Change, Error> {
1819 self.change_op(|ed, out| unsafe {
1820 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
1821 })
1822 }
1823
1824 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
1843 let (block_kind, level) = kind.to_c();
1844 self.change_op(|ed, out| unsafe {
1845 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
1846 })
1847 }
1848
1849 pub fn toggle_block_container(
1872 &mut self,
1873 start: usize,
1874 end: usize,
1875 kind: BlockContainerKind,
1876 ) -> Result<Change, Error> {
1877 self.change_op(|ed, out| unsafe {
1878 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
1879 })
1880 }
1881
1882 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
1901 self.change_op(|ed, out| unsafe {
1902 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
1903 })?;
1904 Ok(())
1905 }
1906
1907 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
1916 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
1917 }
1918
1919 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
1922 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
1923 }
1924
1925 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
1927 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
1928 }
1929
1930 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
1932 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
1933 }
1934
1935 pub fn table_set_alignment(
1937 &mut self,
1938 offset: usize,
1939 alignment: Alignment,
1940 ) -> Result<(), Error> {
1941 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
1942 }
1943
1944 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
1946 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
1947 }
1948
1949 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
1951 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
1952 }
1953
1954 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
1955 self.change_op(|ed, out| unsafe { ffi::twig_editor_table_edit(ed, offset, op, arg, out) })?;
1956 Ok(())
1957 }
1958
1959 pub fn insert_link(
2004 &mut self,
2005 start: usize,
2006 end: usize,
2007 destination: &str,
2008 ) -> Result<Change, Error> {
2009 self.change_op(|ed, out| unsafe {
2010 ffi::twig_editor_insert_link(
2011 ed,
2012 start,
2013 end,
2014 destination.as_ptr(),
2015 destination.len(),
2016 out,
2017 )
2018 })
2019 }
2020
2021 pub fn insert_image(
2042 &mut self,
2043 start: usize,
2044 end: usize,
2045 destination: &str,
2046 ) -> Result<Change, Error> {
2047 self.change_op(|ed, out| unsafe {
2048 ffi::twig_editor_insert_image(
2049 ed,
2050 start,
2051 end,
2052 destination.as_ptr(),
2053 destination.len(),
2054 out,
2055 )
2056 })
2057 }
2058
2059 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
2080 self.change_op(|ed, out| unsafe {
2081 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
2082 })
2083 }
2084
2085 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
2099 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
2100 }
2101
2102 pub fn insert_thematic_break(&mut self, offset: usize) -> Result<Change, Error> {
2121 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_thematic_break(ed, offset, out) })
2122 }
2123
2124 pub fn split_block(&mut self, offset: usize) -> Result<Change, Error> {
2172 self.change_op(|ed, out| unsafe { ffi::twig_editor_split_block(ed, offset, out) })
2173 }
2174
2175 pub fn toggle_code_block(
2207 &mut self,
2208 start: usize,
2209 end: usize,
2210 language: Option<&str>,
2211 ) -> Result<Change, Error> {
2212 let (ptr, len, has) = opt_str(language);
2213 self.change_op(|ed, out| unsafe {
2214 ffi::twig_editor_toggle_code_block(ed, start, end, ptr, len, has, out)
2215 })
2216 }
2217
2218 pub fn set_code_language(
2228 &mut self,
2229 offset: usize,
2230 language: Option<&str>,
2231 ) -> Result<Change, Error> {
2232 let (ptr, len, has) = opt_str(language);
2233 self.change_op(|ed, out| unsafe {
2234 ffi::twig_editor_set_code_language(ed, offset, ptr, len, has, out)
2235 })
2236 }
2237
2238 pub fn toggle_task_item(&mut self, offset: usize) -> Result<Change, Error> {
2249 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_item(ed, offset, out) })
2250 }
2251
2252 pub fn set_task_checked(&mut self, offset: usize, checked: bool) -> Result<(), Error> {
2266 self.change_op(|ed, out| unsafe {
2267 ffi::twig_editor_set_task_checked(ed, offset, checked as c_int, out)
2268 })?;
2269 Ok(())
2270 }
2271
2272 pub fn toggle_task_checked(&mut self, offset: usize) -> Result<Change, Error> {
2277 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_checked(ed, offset, out) })
2278 }
2279
2280 pub fn insert_footnote(&mut self, offset: usize, label: &str) -> Result<Change, Error> {
2299 self.change_op(|ed, out| unsafe {
2300 ffi::twig_editor_insert_footnote(ed, offset, label.as_ptr(), label.len(), out)
2301 })
2302 }
2303
2304 fn change_op(
2307 &mut self,
2308 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
2309 ) -> Result<Change, Error> {
2310 let mut change = ffi::TwigChange {
2311 old_span: ffi::TwigSpan { start: 0, end: 0 },
2312 new_span: ffi::TwigSpan { start: 0, end: 0 },
2313 };
2314 let status = op(self.raw.as_ptr(), &mut change);
2315 Error::from_status(status)?;
2316 Ok(Change::from_ffi(change))
2317 }
2318
2319 fn apply(
2321 &mut self,
2322 locator: &str,
2323 text: &str,
2324 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
2325 ) -> Result<(), Error> {
2326 let status = op(
2327 self.raw.as_ptr(),
2328 locator.as_ptr(),
2329 locator.len(),
2330 text.as_ptr(),
2331 text.len(),
2332 );
2333 Error::from_status(status)
2334 }
2335}
2336
2337impl Drop for Editor {
2338 fn drop(&mut self) {
2339 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
2340 }
2341}
2342
2343fn collect_bytes(
2348 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
2349) -> Result<Vec<u8>, Error> {
2350 let mut ptr = std::ptr::null();
2351 let mut len = 0usize;
2352 let status = call(&mut ptr, &mut len);
2353 Error::from_status(status)?;
2354 if len == 0 {
2355 return Ok(Vec::new());
2356 }
2357 if ptr.is_null() {
2358 return Err(Error::Internal);
2359 }
2360 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2361 Ok(bytes.to_vec())
2362}
2363
2364fn collect_matches(
2367 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
2368) -> Result<Vec<QueryMatch>, Error> {
2369 let mut ptr = std::ptr::null();
2370 let mut len = 0usize;
2371 let status = call(&mut ptr, &mut len);
2372 Error::from_status(status)?;
2373 if len == 0 {
2374 return Ok(Vec::new());
2375 }
2376 if ptr.is_null() {
2377 return Err(Error::Internal);
2378 }
2379 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
2380 matches.iter().map(query_match_from_ffi).collect()
2381}
2382
2383fn collect_flat_nodes(
2386 call: impl FnOnce(*mut *const ffi::TwigFlatNode, *mut usize) -> ffi::TwigStatus,
2387) -> Result<Vec<FlatNode>, Error> {
2388 let mut ptr = std::ptr::null();
2389 let mut len = 0usize;
2390 let status = call(&mut ptr, &mut len);
2391 Error::from_status(status)?;
2392 if len == 0 {
2393 return Ok(Vec::new());
2394 }
2395 if ptr.is_null() {
2396 return Err(Error::Internal);
2397 }
2398 let nodes = unsafe { std::slice::from_raw_parts(ptr, len) };
2399 nodes.iter().map(flat_node_from_ffi).collect()
2400}
2401
2402fn empty_ffi_match() -> ffi::TwigQueryMatch {
2404 ffi::TwigQueryMatch {
2405 node_id: 0,
2406 span: ffi::TwigSpan { start: 0, end: 0 },
2407 content_span: ffi::TwigSpan { start: 0, end: 0 },
2408 has_content_span: 0,
2409 kind: std::ptr::null(),
2410 }
2411}
2412
2413fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
2416 Ok(QueryMatch {
2417 node_id: m.node_id,
2418 span: m.span.start..m.span.end,
2419 content_span: if m.has_content_span != 0 {
2420 Some(m.content_span.start..m.content_span.end)
2421 } else {
2422 None
2423 },
2424 kind: Kind::from(borrowed_cstr(m.kind)?.as_str()),
2425 })
2426}
2427
2428fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
2430 let node_id = |v: u32| {
2431 if v == ffi::TWIG_NO_NODE {
2432 None
2433 } else {
2434 Some(NodeId(v))
2435 }
2436 };
2437 Ok(FlatNode {
2438 id: NodeId(n.id),
2439 parent: node_id(n.parent),
2440 first_child: node_id(n.first_child),
2441 next_sibling: node_id(n.next_sibling),
2442 span: n.span.start..n.span.end,
2443 content_span: if n.has_content_span != 0 {
2444 Some(n.content_span.start..n.content_span.end)
2445 } else {
2446 None
2447 },
2448 level: if n.level != 0 { Some(n.level) } else { None },
2449 kind: Kind::from(borrowed_cstr(n.kind)?.as_str()),
2450 text: borrowed_bytes(n.text_ptr, n.text_len),
2451 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
2452 head: match n.head {
2453 ffi::TWIG_HEAD_NONE => None,
2454 v => Some(v != 0),
2455 },
2456 alignment: Alignment::from_c(n.alignment),
2457 name: borrowed_bytes(n.name_ptr, n.name_len),
2458 directive_form: DirectiveForm::from_c(n.directive_form),
2459 origin: ContainerOrigin::from_c(n.container_origin),
2460 marker_span: if n.has_marker_span != 0 {
2461 Some(n.marker_span.start..n.marker_span.end)
2462 } else {
2463 None
2464 },
2465 checked: match n.checked {
2466 ffi::TWIG_TASK_CHECKED_NONE => None,
2467 v => Some(v != 0),
2468 },
2469 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
2470 })
2471}
2472
2473fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
2477 if ptr.is_null() || len == 0 {
2478 return Vec::new();
2479 }
2480 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
2481 kvs.iter()
2482 .map(|kv| {
2483 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
2484 (key, borrowed_bytes(kv.value, kv.value_len))
2485 })
2486 .collect()
2487}
2488
2489fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
2491 if ptr.is_null() {
2492 return Err(Error::Internal);
2493 }
2494 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
2495 .to_str()
2496 .map_err(|_| Error::Internal)?
2497 .to_owned())
2498}
2499
2500fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
2504 if ptr.is_null() {
2505 return None;
2506 }
2507 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2508 Some(String::from_utf8_lossy(bytes).into_owned())
2509}
2510
2511#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
2515pub struct NodeId(pub u32);
2516
2517#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2520pub enum VoidKind {
2521 Doc,
2522 Para,
2523 ThematicBreak,
2524 Section,
2525 Div,
2526 BlockQuote,
2527 DefinitionList,
2528 Table,
2529 ListItem,
2530 DefinitionListItem,
2531 Term,
2532 Definition,
2533 Caption,
2534 SoftBreak,
2535 HardBreak,
2536 NonBreakingSpace,
2537 Emph,
2538 Strong,
2539 Span,
2540 Mark,
2541 Superscript,
2542 Subscript,
2543 Insert,
2544 Delete,
2545 DoubleQuoted,
2546 SingleQuoted,
2547}
2548
2549impl VoidKind {
2550 fn to_c(self) -> c_int {
2551 match self {
2553 VoidKind::Doc => 0,
2554 VoidKind::Para => 1,
2555 VoidKind::ThematicBreak => 3,
2556 VoidKind::Section => 4,
2557 VoidKind::Div => 5,
2558 VoidKind::BlockQuote => 9,
2559 VoidKind::DefinitionList => 13,
2560 VoidKind::Table => 14,
2561 VoidKind::ListItem => 15,
2562 VoidKind::DefinitionListItem => 17,
2563 VoidKind::Term => 18,
2564 VoidKind::Definition => 19,
2565 VoidKind::Caption => 22,
2566 VoidKind::SoftBreak => 26,
2567 VoidKind::HardBreak => 27,
2568 VoidKind::NonBreakingSpace => 28,
2569 VoidKind::Emph => 38,
2570 VoidKind::Strong => 39,
2571 VoidKind::Span => 42,
2572 VoidKind::Mark => 43,
2573 VoidKind::Superscript => 44,
2574 VoidKind::Subscript => 45,
2575 VoidKind::Insert => 46,
2576 VoidKind::Delete => 47,
2577 VoidKind::DoubleQuoted => 48,
2578 VoidKind::SingleQuoted => 49,
2579 }
2580 }
2581}
2582
2583#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2585pub enum TextKind {
2586 Str,
2587 Symb,
2588 Verbatim,
2589 InlineMath,
2590 DisplayMath,
2591 Url,
2592 Email,
2593 FootnoteReference,
2594 CitationReference,
2597 SubstitutionReference,
2599 Comment,
2600 Doctype,
2601 Cdata,
2602}
2603
2604impl TextKind {
2605 fn to_c(self) -> c_int {
2606 match self {
2607 TextKind::Str => 25,
2608 TextKind::Symb => 29,
2609 TextKind::Verbatim => 30,
2610 TextKind::InlineMath => 32,
2611 TextKind::DisplayMath => 33,
2612 TextKind::Url => 34,
2613 TextKind::Email => 35,
2614 TextKind::FootnoteReference => 36,
2615 TextKind::CitationReference => 58,
2616 TextKind::SubstitutionReference => 59,
2617 TextKind::Comment => 52,
2618 TextKind::Doctype => 53,
2619 TextKind::Cdata => 55,
2620 }
2621 }
2622}
2623
2624#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2626pub enum BulletStyle {
2627 Dash,
2628 Plus,
2629 Star,
2630}
2631
2632impl BulletStyle {
2633 fn to_c(self) -> c_int {
2634 match self {
2635 BulletStyle::Dash => 0,
2636 BulletStyle::Plus => 1,
2637 BulletStyle::Star => 2,
2638 }
2639 }
2640}
2641
2642#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2644pub enum OrderedNumbering {
2645 Decimal,
2646 LowerAlpha,
2647 UpperAlpha,
2648 LowerRoman,
2649 UpperRoman,
2650}
2651
2652impl OrderedNumbering {
2653 fn to_c(self) -> c_int {
2654 match self {
2655 OrderedNumbering::Decimal => 0,
2656 OrderedNumbering::LowerAlpha => 1,
2657 OrderedNumbering::UpperAlpha => 2,
2658 OrderedNumbering::LowerRoman => 3,
2659 OrderedNumbering::UpperRoman => 4,
2660 }
2661 }
2662}
2663
2664#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2666pub enum OrderedDelim {
2667 Period,
2668 ParenAfter,
2669 ParenBoth,
2670}
2671
2672impl OrderedDelim {
2673 fn to_c(self) -> c_int {
2674 match self {
2675 OrderedDelim::Period => 0,
2676 OrderedDelim::ParenAfter => 1,
2677 OrderedDelim::ParenBoth => 2,
2678 }
2679 }
2680}
2681
2682#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2685pub enum Alignment {
2686 Default,
2687 Left,
2688 Right,
2689 Center,
2690}
2691
2692impl Alignment {
2693 fn to_c(self) -> c_int {
2694 match self {
2695 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
2696 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
2697 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
2698 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
2699 }
2700 }
2701
2702 fn from_c(v: c_int) -> Option<Self> {
2705 match v {
2706 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
2707 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
2708 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
2709 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
2710 _ => None,
2711 }
2712 }
2713}
2714
2715#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2717pub enum SmartPunctuation {
2718 LeftSingleQuote,
2719 RightSingleQuote,
2720 LeftDoubleQuote,
2721 RightDoubleQuote,
2722 Ellipses,
2723 EmDash,
2724 EnDash,
2725}
2726
2727impl SmartPunctuation {
2728 fn to_c(self) -> c_int {
2729 match self {
2730 SmartPunctuation::LeftSingleQuote => 0,
2731 SmartPunctuation::RightSingleQuote => 1,
2732 SmartPunctuation::LeftDoubleQuote => 2,
2733 SmartPunctuation::RightDoubleQuote => 3,
2734 SmartPunctuation::Ellipses => 4,
2735 SmartPunctuation::EmDash => 5,
2736 SmartPunctuation::EnDash => 6,
2737 }
2738 }
2739}
2740
2741#[derive(Clone, Debug, Eq, PartialEq)]
2756pub struct Warning {
2757 pub fidelity: Fidelity,
2758 pub path: String,
2765 pub kind: Kind,
2768}
2769
2770#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2772#[non_exhaustive]
2773pub enum Fidelity {
2774 Degraded,
2777 Dropped,
2779}
2780
2781impl Fidelity {
2782 fn from_c(v: c_int) -> Self {
2786 match v {
2787 ffi::TWIG_FIDELITY_DROPPED => Fidelity::Dropped,
2788 _ => Fidelity::Degraded,
2789 }
2790 }
2791}
2792
2793#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2794#[non_exhaustive]
2795pub enum ContainerOrigin {
2796 Element,
2798 Directive,
2802}
2803
2804impl ContainerOrigin {
2805 fn from_c(v: c_int) -> Option<Self> {
2808 match v {
2809 ffi::TWIG_CONTAINER_ORIGIN_ELEMENT => Some(ContainerOrigin::Element),
2810 ffi::TWIG_CONTAINER_ORIGIN_DIRECTIVE => Some(ContainerOrigin::Directive),
2811 _ => None,
2812 }
2813 }
2814}
2815
2816#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2818pub enum DirectiveForm {
2819 Text,
2820 Leaf,
2821 Container,
2822}
2823
2824impl DirectiveForm {
2825 fn to_c(self) -> c_int {
2826 match self {
2827 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
2828 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
2829 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
2830 }
2831 }
2832
2833 fn from_c(v: c_int) -> Option<Self> {
2837 match v {
2838 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
2839 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
2840 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
2841 _ => None,
2842 }
2843 }
2844}
2845
2846fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
2850 match s {
2851 Some(x) => (x.as_ptr(), x.len(), 1),
2852 None => (std::ptr::null(), 0, 0),
2853 }
2854}
2855
2856#[derive(Debug)]
2863pub struct Builder {
2864 raw: NonNull<ffi::TwigBuilder>,
2865}
2866
2867impl Builder {
2868 pub fn new() -> Result<Self, Error> {
2870 let mut raw = std::ptr::null_mut();
2871 let status = unsafe { ffi::twig_builder_create(&mut raw) };
2872 Error::from_status(status)?;
2873 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
2874 Ok(Self { raw })
2875 }
2876
2877 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
2880 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
2881 }
2882
2883 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
2885 self.emit(|b, out| unsafe {
2886 ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out)
2887 })
2888 }
2889
2890 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
2892 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
2893 }
2894
2895 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
2897 let (lp, ll, has) = opt_str(lang);
2898 self.emit(|b, out| unsafe {
2899 ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out)
2900 })
2901 }
2902
2903 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
2905 self.emit(|b, out| unsafe {
2906 ffi::twig_builder_add_raw_block(
2907 b,
2908 format.as_ptr(),
2909 format.len(),
2910 text.as_ptr(),
2911 text.len(),
2912 out,
2913 )
2914 })
2915 }
2916
2917 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
2919 self.emit(|b, out| unsafe {
2920 ffi::twig_builder_add_metadata(
2921 b,
2922 lang.as_ptr(),
2923 lang.len(),
2924 text.as_ptr(),
2925 text.len(),
2926 out,
2927 )
2928 })
2929 }
2930
2931 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
2933 self.emit(|b, out| unsafe {
2934 ffi::twig_builder_add_raw_inline(
2935 b,
2936 format.as_ptr(),
2937 format.len(),
2938 text.as_ptr(),
2939 text.len(),
2940 out,
2941 )
2942 })
2943 }
2944
2945 pub fn add_smart_punctuation(
2950 &mut self,
2951 kind: SmartPunctuation,
2952 text: &str,
2953 ) -> Result<NodeId, Error> {
2954 self.emit(|b, out| unsafe {
2955 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
2956 })
2957 }
2958
2959 pub fn add_link(
2962 &mut self,
2963 destination: Option<&str>,
2964 reference: Option<&str>,
2965 ) -> Result<NodeId, Error> {
2966 let (dp, dl, hd) = opt_str(destination);
2967 let (rp, rl, hr) = opt_str(reference);
2968 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
2969 }
2970
2971 pub fn add_image(
2973 &mut self,
2974 destination: Option<&str>,
2975 reference: Option<&str>,
2976 ) -> Result<NodeId, Error> {
2977 let (dp, dl, hd) = opt_str(destination);
2978 let (rp, rl, hr) = opt_str(reference);
2979 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
2980 }
2981
2982 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
2984 self.emit(|b, out| unsafe {
2985 ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out)
2986 })
2987 }
2988
2989 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
2991 self.emit(|b, out| unsafe {
2992 ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out)
2993 })
2994 }
2995
2996 pub fn add_processing_instruction(
2998 &mut self,
2999 target: &str,
3000 data: &str,
3001 ) -> Result<NodeId, Error> {
3002 self.emit(|b, out| unsafe {
3003 ffi::twig_builder_add_processing_instruction(
3004 b,
3005 target.as_ptr(),
3006 target.len(),
3007 data.as_ptr(),
3008 data.len(),
3009 out,
3010 )
3011 })
3012 }
3013
3014 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
3016 self.emit(|b, out| unsafe {
3017 ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out)
3018 })
3019 }
3020
3021 pub fn add_citation(&mut self, label: &str) -> Result<NodeId, Error> {
3026 self.emit(|b, out| unsafe {
3027 ffi::twig_builder_add_citation(b, label.as_ptr(), label.len(), out)
3028 })
3029 }
3030
3031 pub fn add_substitution(&mut self, label: &str) -> Result<NodeId, Error> {
3035 self.emit(|b, out| unsafe {
3036 ffi::twig_builder_add_substitution(b, label.as_ptr(), label.len(), out)
3037 })
3038 }
3039
3040 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
3042 self.emit(|b, out| unsafe {
3043 ffi::twig_builder_add_reference(
3044 b,
3045 label.as_ptr(),
3046 label.len(),
3047 destination.as_ptr(),
3048 destination.len(),
3049 out,
3050 )
3051 })
3052 }
3053
3054 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
3056 self.emit(|b, out| unsafe {
3057 ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out)
3058 })
3059 }
3060
3061 pub fn add_ordered_list(
3063 &mut self,
3064 numbering: OrderedNumbering,
3065 delim: OrderedDelim,
3066 tight: bool,
3067 start: Option<u32>,
3068 ) -> Result<NodeId, Error> {
3069 let (start_val, has_start) = match start {
3070 Some(s) => (s, 1),
3071 None => (0, 0),
3072 };
3073 self.emit(|b, out| unsafe {
3074 ffi::twig_builder_add_ordered_list(
3075 b,
3076 numbering.to_c(),
3077 delim.to_c(),
3078 tight as c_int,
3079 start_val,
3080 has_start,
3081 out,
3082 )
3083 })
3084 }
3085
3086 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
3088 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
3089 }
3090
3091 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
3093 self.emit(|b, out| unsafe {
3094 ffi::twig_builder_add_task_list_item(b, checked as c_int, out)
3095 })
3096 }
3097
3098 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
3100 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
3101 }
3102
3103 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
3105 self.emit(|b, out| unsafe {
3106 ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out)
3107 })
3108 }
3109
3110 pub fn add_cell_spanning(
3115 &mut self,
3116 head: bool,
3117 alignment: Alignment,
3118 colspan: u32,
3119 rowspan: u32,
3120 ) -> Result<NodeId, Error> {
3121 self.emit(|b, out| unsafe {
3122 ffi::twig_builder_add_cell_spanning(
3123 b,
3124 head as c_int,
3125 alignment.to_c(),
3126 colspan,
3127 rowspan,
3128 out,
3129 )
3130 })
3131 }
3132
3133 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
3136 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
3137 let status = unsafe {
3138 ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len())
3139 };
3140 Error::from_status(status)
3141 }
3142
3143 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
3147 let kvs: Vec<ffi::TwigKeyVal> = attrs
3148 .iter()
3149 .map(|(k, v)| ffi::TwigKeyVal {
3150 key: k.as_ptr(),
3151 key_len: k.len(),
3152 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
3153 value_len: v.map_or(0, |s| s.len()),
3154 })
3155 .collect();
3156 let status = unsafe {
3157 ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len())
3158 };
3159 Error::from_status(status)
3160 }
3161
3162 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3165 let raw = self.raw.as_ptr();
3166 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
3167 }
3168
3169 pub fn serialize_to(&mut self, root: NodeId, target: Target) -> Result<Vec<u8>, Error> {
3176 let raw = self.raw.as_ptr();
3177 let ffi_target: ffi::TwigFormat = target.into();
3178 collect_bytes(|ptr, len| unsafe {
3179 ffi::twig_builder_serialize(raw, root.0, ffi_target as i32, ptr, len)
3180 })
3181 }
3182
3183 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
3188 self.serialize_to(root, format.into())
3189 }
3190
3191 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3193 let raw = self.raw.as_ptr();
3194 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
3195 }
3196
3197 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
3200 let raw = self.raw.as_ptr();
3201 collect_matches(|ptr, len| unsafe {
3202 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
3203 })
3204 }
3205
3206 fn emit(
3209 &mut self,
3210 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
3211 ) -> Result<NodeId, Error> {
3212 let mut id: u32 = 0;
3213 let status = call(self.raw.as_ptr(), &mut id);
3214 Error::from_status(status)?;
3215 Ok(NodeId(id))
3216 }
3217}
3218
3219impl Drop for Builder {
3220 fn drop(&mut self) {
3221 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
3222 }
3223}
3224
3225#[cfg(test)]
3226mod tests {
3227 use super::*;
3228
3229 #[test]
3230 fn abi_version_matches() {
3231 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
3235 }
3236
3237 #[test]
3238 fn parses_and_renders_markdown_html() {
3239 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3240 let html = doc.render_html().expect("render html");
3241 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
3242 }
3243
3244 #[test]
3245 fn parses_html_input() {
3246 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
3247 let html = doc.render_html().expect("render html");
3248 assert!(String::from_utf8_lossy(&html).contains("hi"));
3249 }
3250
3251 #[test]
3252 fn parses_asciidoc_and_refuses_to_write_it() {
3253 let mut doc = Document::parse_str("= Title\n\nsome *bold* text\n", Format::Asciidoc)
3254 .expect("parse asciidoc");
3255 let html = String::from_utf8_lossy(&doc.render_html().expect("render html")).into_owned();
3256 assert!(html.contains("<h1>Title</h1>"), "got {html:?}");
3257 assert!(html.contains("<strong>bold</strong>"), "got {html:?}");
3258
3259 assert_eq!(
3263 doc.serialize_to(Target::Asciidoc),
3264 Err(Error::UnsupportedFormat)
3265 );
3266 assert_eq!(Target::from(Format::Asciidoc), Target::Asciidoc);
3267 assert_eq!(Target::Asciidoc.as_format(), Some(Format::Asciidoc));
3268 }
3269
3270 #[test]
3271 fn serialize_round_trips_and_cross_converts() {
3272 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3273
3274 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
3275 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
3276
3277 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
3279 }
3280
3281 #[test]
3282 fn serialize_markdown_to_djot() {
3283 let mut doc =
3284 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3285 let djot = doc.serialize(Format::Djot).expect("serialize djot");
3286 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3287 }
3288
3289 #[test]
3290 fn serialize_to_takes_the_output_axis() {
3291 let mut doc =
3292 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3293
3294 let djot = doc.serialize_to(Target::Djot).expect("serialize djot");
3295 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3296
3297 assert_eq!(doc.serialize_to(Target::Xml), Err(Error::UnsupportedFormat));
3300 }
3301
3302 #[test]
3303 fn serialize_and_serialize_to_agree() {
3304 let mut a = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3307 let mut b = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3308 for format in [Format::Markdown, Format::Djot, Format::Html] {
3309 assert_eq!(a.serialize(format), b.serialize_to(Target::from(format)));
3310 }
3311 }
3312
3313 #[test]
3314 fn every_format_is_a_target_that_names_it_back() {
3315 for format in [Format::Djot, Format::Markdown, Format::Xml, Format::Html] {
3318 assert_eq!(Target::from(format).as_format(), Some(format));
3319 }
3320 }
3321
3322 #[test]
3323 fn ast_json_dumps_the_tree() {
3324 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
3325 let json = doc.ast_json().expect("ast json");
3326 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
3327 }
3328
3329 #[test]
3330 fn query_finds_nodes_by_selector() {
3331 let source = "# One\n\n## Two\n";
3332 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3333 let matches = doc.query("heading").expect("query");
3334
3335 assert_eq!(matches.len(), 2);
3336 for m in &matches {
3337 assert_eq!(m.kind, Kind::Heading);
3338 assert!(m.span.start < m.span.end);
3339 }
3340 }
3341
3342 #[test]
3343 fn query_recovers_code_spans() {
3344 let source = "prose `code` more prose\n";
3345 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3346 let matches = doc.query("verbatim").expect("query");
3347
3348 assert_eq!(matches.len(), 1);
3349 assert_eq!(&source[matches[0].span.clone()], "`code`");
3350 }
3351
3352 #[test]
3353 fn document_span_accessors_read_by_node_id() {
3354 let source = "# hi\n\ntext\n";
3355 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3356 let heading = doc.query("heading").expect("query").pop().expect("heading");
3357
3358 assert_eq!(
3359 doc.span(NodeId(heading.node_id)).expect("span"),
3360 heading.span
3361 );
3362 assert_eq!(
3363 doc.content_span(NodeId(heading.node_id))
3364 .expect("content span"),
3365 heading.content_span
3366 );
3367 assert_eq!(doc.span(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3368 }
3369
3370 #[test]
3371 fn document_walks_its_tree_without_an_editor() {
3372 let source = "# hi\n\ntext\n";
3373 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3374
3375 let nodes = doc.nodes().expect("nodes");
3376 assert!(nodes.len() >= 3);
3377 for (i, n) in nodes.iter().enumerate() {
3378 assert_eq!(n.id, NodeId(i as u32));
3379 }
3380
3381 let kids = doc.children(None).expect("children");
3382 assert_eq!(kids.len(), 2);
3383 assert_eq!(kids[0].kind, Kind::Heading);
3384
3385 let sub = doc.subtree(NodeId(kids[0].node_id)).expect("subtree");
3386 assert_eq!(sub[0].id, NodeId(0));
3387 assert_eq!(sub[0].parent, None);
3388 assert_eq!(sub[0].span, kids[0].span);
3389
3390 let hit = doc.node_at(2).expect("node_at").expect("a node at 2");
3391 let chain = doc.ancestors_at(2).expect("ancestors");
3392 assert_eq!(chain.last().expect("deepest").node_id, hit.node_id);
3393 assert_eq!(chain[0].kind, Kind::Doc);
3394
3395 assert_eq!(doc.subtree(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3396 }
3397
3398 #[test]
3399 fn editor_document_view_reads_the_live_tree() {
3400 let mut ed = Editor::new_str("# one\n\ntwo\n", Format::Markdown).expect("editor");
3401
3402 {
3403 let mut view = ed.document().expect("view");
3404 let kids = view.children(None).expect("children");
3405 assert_eq!(kids.len(), 2);
3406 assert_eq!(kids[0].kind, Kind::Heading);
3407 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..5);
3408 assert_eq!(view.render_html(), Err(Error::UnsupportedFormat));
3410 assert_eq!(
3411 view.serialize(Format::Markdown),
3412 Err(Error::UnsupportedFormat)
3413 );
3414 }
3415
3416 ed.replace("0", "# one and a half").expect("replace");
3417 let mut view = ed.document().expect("view");
3418 let kids = view.children(None).expect("children");
3419 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..16);
3420 }
3421
3422 #[test]
3423 fn query_rejects_a_malformed_selector() {
3424 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
3425 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
3426 }
3427
3428 #[test]
3429 fn editor_edits_by_index_path() {
3430 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
3431 ed.replace_content("0.0", "bye").expect("replace_content");
3432 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
3433 }
3434
3435 #[test]
3436 fn flat_nodes_expose_element_name_and_attrs() {
3437 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
3441 let mut ed = Editor::new_ext(
3442 src.as_bytes(),
3443 Format::Markdown,
3444 MarkdownExtensions {
3445 html_elements: true,
3446 ..Default::default()
3447 },
3448 )
3449 .expect("editor");
3450 let nodes = ed.nodes().expect("nodes");
3451
3452 let source = nodes
3453 .iter()
3454 .find(|n| n.name.as_deref() == Some("source"))
3455 .expect("a <source> element node");
3456 assert_eq!(
3457 source.attrs,
3458 vec![
3459 (
3460 "media".to_string(),
3461 Some("(prefers-color-scheme: dark)".to_string())
3462 ),
3463 ("srcset".to_string(), Some("d.svg".to_string())),
3464 ]
3465 );
3466
3467 let img = nodes
3470 .iter()
3471 .find(|n| n.kind == Kind::Image)
3472 .expect("an image node");
3473 assert!(img.name.is_none());
3474 assert_eq!(img.destination.as_deref(), Some("l.svg"));
3475
3476 let picture_kids_str = nodes.iter().find(|n| n.kind == Kind::Str);
3478 if let Some(s) = picture_kids_str {
3479 assert!(s.name.is_none() && s.attrs.is_empty());
3480 }
3481 }
3482
3483 #[test]
3484 fn definitions_finds_what_a_walk_from_the_root_cannot() {
3485 let mut doc = Document::parse_str(
3489 "text[^1] [x][a]\n\n[^1]: note\n\n[a]: /u\n",
3490 Format::Markdown,
3491 )
3492 .expect("parse markdown");
3493
3494 let defs = doc.definitions().expect("definitions");
3495 let mut kinds: Vec<Kind> = defs.iter().map(|m| m.kind.clone()).collect();
3496 kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3497 assert_eq!(kinds, vec![Kind::Footnote, Kind::Reference]);
3498
3499 let all = doc.nodes().expect("nodes");
3502 let root = all
3503 .iter()
3504 .find(|n| n.kind == Kind::Doc)
3505 .expect("a doc root");
3506 let mut reachable = vec![root.id];
3507 let mut i = 0;
3508 while i < reachable.len() {
3509 let n = &all[reachable[i].0 as usize];
3510 let mut c = n.first_child;
3511 while let Some(cid) = c {
3512 reachable.push(cid);
3513 c = all[cid.0 as usize].next_sibling;
3514 }
3515 i += 1;
3516 }
3517 for d in &defs {
3518 assert!(
3519 !reachable.contains(&NodeId(d.node_id)),
3520 "{} should be unreachable from the root",
3521 d.kind
3522 );
3523 }
3524
3525 let mut plain = Document::parse_str("just text\n", Format::Markdown).expect("parse");
3527 assert_eq!(plain.definitions().expect("definitions"), Vec::new());
3528 }
3529
3530 #[test]
3531 fn kind_round_trips_through_its_published_name() {
3532 for k in [
3536 Kind::Doc,
3537 Kind::Para,
3538 Kind::Heading,
3539 Kind::Container,
3540 Kind::TaskListItem,
3541 Kind::Superscript,
3542 Kind::FootnoteReference,
3543 Kind::ProcessingInstruction,
3544 Kind::Cdata,
3545 ] {
3546 assert_eq!(Kind::from(k.as_str()), k, "{k} did not round-trip");
3547 assert!(!k.is_unknown());
3548 }
3549 }
3550
3551 #[test]
3552 fn an_unknown_kind_name_is_carried_rather_than_lost() {
3553 let k = Kind::from("some_future_kind");
3556 assert!(k.is_unknown());
3557 assert_eq!(k.as_str(), "some_future_kind");
3558 assert_eq!(k, Kind::Other("some_future_kind".to_string()));
3559 }
3560
3561 #[test]
3562 fn every_kind_the_library_publishes_has_a_variant() {
3563 let cases: &[(&str, Format, MarkdownExtensions)] = &[
3568 (
3569 "# 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",
3570 Format::Markdown,
3571 MarkdownExtensions {
3572 directives: false,
3573 math: false,
3574 html_elements: false,
3575 },
3576 ),
3577 (
3578 "| 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",
3579 Format::Markdown,
3580 MarkdownExtensions::default(),
3581 ),
3582 (
3583 ":::note\nbody\n:::\n\n:role[x]\n\n$a+b$\n",
3584 Format::Markdown,
3585 MarkdownExtensions {
3586 directives: true,
3587 math: true,
3588 html_elements: false,
3589 },
3590 ),
3591 (
3592 "a^b^ c~d~ {=e=} {+f+} {-g-} 'q' \"dq\"\n\n\n\n<https://e.com>\n",
3593 Format::Djot,
3594 MarkdownExtensions::default(),
3595 ),
3596 (
3597 "<!-- c --><!DOCTYPE html><video controls><p>x</p></video>",
3598 Format::Html,
3599 MarkdownExtensions::default(),
3600 ),
3601 ];
3602
3603 let mut unknown: Vec<String> = Vec::new();
3604 let mut seen: Vec<String> = Vec::new();
3605 for (src, format, ext) in cases {
3606 let mut ed = Editor::new_ext(src.as_bytes(), *format, *ext).expect("editor");
3607 for n in ed.nodes().expect("nodes") {
3608 if n.kind.is_unknown() {
3609 unknown.push(n.kind.as_str().to_string());
3610 }
3611 seen.push(n.kind.as_str().to_string());
3612 }
3613 }
3614 unknown.sort();
3615 unknown.dedup();
3616 assert!(unknown.is_empty(), "kinds with no variant: {unknown:?}");
3617
3618 seen.sort();
3621 seen.dedup();
3622 assert!(
3623 seen.len() >= 30,
3624 "only {} distinct kinds reached: {seen:?}",
3625 seen.len()
3626 );
3627 }
3628
3629 #[test]
3630 fn diagnostics_report_what_a_conversion_would_lose() {
3631 let mut doc = Document::parse_str("a^b^ c\n", Format::Djot).expect("parse djot");
3635
3636 let to_md = doc
3637 .diagnostics(Target::Markdown)
3638 .expect("markdown diagnostics");
3639 assert_eq!(
3640 to_md,
3641 vec![Warning {
3642 fidelity: Fidelity::Degraded,
3643 path: "0/1".to_string(),
3644 kind: Kind::Superscript,
3645 }]
3646 );
3647
3648 assert_eq!(
3650 doc.diagnostics(Target::Djot).expect("djot diagnostics"),
3651 Vec::new()
3652 );
3653 }
3654
3655 #[test]
3656 fn diagnostics_separate_a_droppable_node_from_a_degradable_one() {
3657 let mut doc =
3661 Document::parse_str("<p>hi</p><!-- secret -->", Format::Html).expect("parse html");
3662 let warnings = doc.diagnostics(Target::Djot).expect("djot diagnostics");
3663 let comment = warnings
3664 .iter()
3665 .find(|w| w.kind == Kind::Comment)
3666 .expect("a warning about the comment");
3667 assert_eq!(comment.fidelity, Fidelity::Dropped);
3668 }
3669
3670 #[test]
3671 fn diagnostics_refuse_a_target_with_no_serializer() {
3672 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3675 assert_eq!(doc.diagnostics(Target::Xml), Err(Error::UnsupportedFormat));
3676 assert_eq!(
3677 doc.diagnostics(Target::Asciidoc),
3678 Err(Error::UnsupportedFormat)
3679 );
3680 }
3681
3682 #[test]
3683 fn diagnostics_flag_a_header_less_table_and_leave_a_headed_one_alone() {
3684 let mut headed = Document::parse_str(
3689 "<table><tr><th>H</th></tr><tr><td>a</td></tr></table>",
3690 Format::Html,
3691 )
3692 .expect("parse headed table");
3693 assert!(
3694 headed
3695 .diagnostics(Target::Markdown)
3696 .expect("diagnostics")
3697 .iter()
3698 .all(|w| w.kind != Kind::Table)
3699 );
3700
3701 let mut headless = Document::parse_str("<table><tr><td>a</td></tr></table>", Format::Html)
3702 .expect("parse header-less table");
3703 let table_warning = headless
3704 .diagnostics(Target::Markdown)
3705 .expect("diagnostics")
3706 .into_iter()
3707 .find(|w| w.kind == Kind::Table)
3708 .expect("a warning about the table");
3709 assert_eq!(table_warning.fidelity, Fidelity::Degraded);
3710 }
3711
3712 #[test]
3713 fn container_origin_separates_a_div_from_a_div() {
3714 let mut html =
3719 Editor::new("<div>hi</div>\n".as_bytes(), Format::Html).expect("html editor");
3720 let mut md = Editor::new_ext(
3721 ":::div\nhi\n:::\n".as_bytes(),
3722 Format::Markdown,
3723 MarkdownExtensions {
3724 directives: true,
3725 ..Default::default()
3726 },
3727 )
3728 .expect("markdown editor");
3729
3730 let html_nodes = html.nodes().expect("html nodes");
3731 let md_nodes = md.nodes().expect("markdown nodes");
3732 let tag = html_nodes
3733 .iter()
3734 .find(|n| n.name.as_deref() == Some("div"))
3735 .expect("a <div> container");
3736 let directive = md_nodes
3737 .iter()
3738 .find(|n| n.name.as_deref() == Some("div"))
3739 .expect("a :::div container");
3740
3741 assert_eq!(tag.kind, directive.kind);
3743 assert_eq!(tag.name, directive.name);
3744 assert_eq!(tag.directive_form, directive.directive_form);
3745 assert_eq!(tag.directive_form, Some(DirectiveForm::Container));
3746
3747 assert_eq!(tag.origin, Some(ContainerOrigin::Element));
3749 assert_eq!(directive.origin, Some(ContainerOrigin::Directive));
3750 }
3751
3752 fn for_both_formats(src: &str, check: impl Fn(&mut Document, Format)) {
3756 for format in [Format::Markdown, Format::Djot] {
3757 let mut doc = Document::parse(src.as_bytes(), format).expect("parse");
3758 check(&mut doc, format);
3759 }
3760 }
3761
3762 #[test]
3763 fn marker_span_is_what_a_rich_view_hides() {
3764 for_both_formats("> - [x] done\n", |doc, format| {
3765 let nodes = doc.nodes().expect("nodes");
3766 let quote = nodes
3767 .iter()
3768 .find(|n| n.kind == Kind::BlockQuote)
3769 .expect("a block quote");
3770 let item = nodes
3771 .iter()
3772 .find(|n| n.kind == Kind::TaskListItem)
3773 .expect("a task item");
3774
3775 assert_eq!(quote.marker_span, Some(0..2), "{format:?}");
3779 assert_eq!(item.marker_span, Some(2..8), "{format:?}");
3780
3781 assert_eq!(quote.content_span, Some(quote.span.clone()), "{format:?}");
3785
3786 let para = nodes
3788 .iter()
3789 .find(|n| n.kind == Kind::Para)
3790 .expect("a paragraph");
3791 assert_eq!(para.marker_span, None, "{format:?}");
3792 });
3793 }
3794
3795 #[test]
3796 fn attrs_span_locates_the_attribute_block_a_heuristic_had_to_guess_at() {
3797 let src = "{.vis .family}\nheld back\n\nplain\n";
3803 let mut doc = Document::parse(src.as_bytes(), Format::Djot).expect("parse");
3804 let nodes = doc.nodes().expect("nodes");
3805 let paras: Vec<&FlatNode> = nodes.iter().filter(|n| n.kind == Kind::Para).collect();
3806 assert_eq!(paras.len(), 2);
3807
3808 let span = doc
3809 .attrs_span(paras[0].id)
3810 .expect("attrs span")
3811 .expect("the attributed paragraph has one");
3812 assert_eq!(&src[span.clone()], "{.vis .family}");
3813 assert!(span.end <= paras[0].span.start);
3816
3817 assert_eq!(doc.attrs_span(paras[1].id).expect("attrs span"), None);
3820 }
3821
3822 #[test]
3823 fn line_prefix_assembles_every_marker_on_the_line() {
3824 for_both_formats("> - [x] done\n", |doc, format| {
3825 assert_eq!(doc.line_prefix(9).expect("prefix"), Some(0..8), "{format:?}");
3828 });
3829 }
3830
3831 #[test]
3832 fn line_prefix_is_none_on_a_continuation_line() {
3833 for_both_formats("> c\n> d\n", |doc, format| {
3839 assert_eq!(doc.line_prefix(2).expect("prefix"), Some(0..2), "{format:?}");
3840 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
3841 });
3842 }
3843
3844 #[test]
3845 fn a_caret_at_a_blocks_end_is_in_that_block_in_both_formats() {
3846 for_both_formats("a\n\nb\n", |doc, format| {
3852 for offset in [0usize, 1, 3, 4] {
3853 let hit = doc
3854 .node_at_caret(offset)
3855 .expect("caret hit")
3856 .expect("some node");
3857 assert_eq!(hit.kind, Kind::Str, "{format:?} at {offset}");
3858 }
3859 for offset in [2usize, 5] {
3862 let hit = doc
3863 .node_at_caret(offset)
3864 .expect("caret hit")
3865 .expect("some node");
3866 assert_eq!(hit.kind, Kind::Doc, "{format:?} at {offset}");
3867 }
3868 });
3869 }
3870
3871 #[test]
3872 fn the_caret_chain_ends_at_the_node_the_scalar_call_returns() {
3873 for_both_formats("- a\n", |doc, format| {
3874 let hit = doc.node_at_caret(3).expect("hit").expect("some node");
3875 let chain = doc.ancestors_at_caret(3).expect("chain");
3876 assert_eq!(chain.last().map(|m| m.node_id), Some(hit.node_id), "{format:?}");
3877 assert!(
3880 chain.iter().any(|m| m.kind == Kind::ListItem),
3881 "{format:?}: chain should reach the list item"
3882 );
3883 });
3884 }
3885
3886 #[test]
3887 fn continuation_prefix_repeats_a_quote_and_indents_past_an_item() {
3888 for_both_formats("> - a\n", |doc, format| {
3889 assert_eq!(doc.line_prefix(4).expect("prefix"), Some(0..4), "{format:?}");
3894 let cont = doc.continuation_prefix(4).expect("continuation");
3895 assert_eq!(cont.text, "> ", "{format:?}");
3896 assert_eq!(cont.columns, 4, "{format:?}");
3897 });
3898 }
3899
3900 #[test]
3901 fn continuation_prefix_answers_on_a_line_that_opens_nothing() {
3902 for_both_formats("> c\n> d\n", |doc, format| {
3905 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
3906 assert_eq!(
3907 doc.continuation_prefix(6).expect("continuation").text,
3908 "> ",
3909 "{format:?}"
3910 );
3911 });
3912 }
3913
3914 #[test]
3915 fn continuation_prefix_takes_an_ordered_markers_own_width() {
3916 for_both_formats("10. x\n", |doc, format| {
3919 assert_eq!(
3920 doc.continuation_prefix(4).expect("continuation").columns,
3921 4,
3922 "{format:?}"
3923 );
3924 });
3925 for_both_formats("1. x\n", |doc, format| {
3926 assert_eq!(
3927 doc.continuation_prefix(3).expect("continuation").columns,
3928 3,
3929 "{format:?}"
3930 );
3931 });
3932 }
3933
3934 #[test]
3935 fn a_blank_line_keeps_a_quote_alive_and_drops_an_items_indent() {
3936 for_both_formats("> - a\n", |doc, format| {
3937 let blank = doc.blank_line_prefix(4).expect("blank");
3938 assert_eq!(blank.text, ">", "{format:?}");
3941 assert_eq!(blank.columns, 1, "{format:?}");
3942 });
3943 for_both_formats("- a\n", |doc, format| {
3946 assert_eq!(doc.blank_line_prefix(3).expect("blank").text, "", "{format:?}");
3947 });
3948 }
3949
3950 #[test]
3951 fn a_prefix_column_count_is_not_its_byte_length() {
3952 let mut doc = Document::parse("- x
3955".as_bytes(), Format::Markdown).expect("parse");
3956 let cont = doc.continuation_prefix(2).expect("continuation");
3957 assert_eq!(cont.columns, 4);
3958 }
3959
3960 #[test]
3961 fn set_block_opens_a_heading_on_a_blank_line() {
3962 for format in [Format::Markdown, Format::Djot] {
3963 let mut ed = Editor::new("a\n\n".as_bytes(), format).expect("editor");
3964 ed.set_block(3, BlockKind::Heading(2)).expect("set_block");
3965 assert_eq!(ed.source().expect("source"), b"a\n\n## ", "{format:?}");
3966 let nodes = ed.nodes().expect("nodes");
3970 assert!(
3971 nodes.iter().any(|n| n.kind == Kind::Heading),
3972 "{format:?}: should have parsed a heading"
3973 );
3974 }
3975 }
3976
3977 #[test]
3978 fn set_block_refuses_a_blank_line_inside_a_code_block() {
3979 for format in [Format::Markdown, Format::Djot] {
3983 let src = "```\nx\n\ny\n```\n";
3984 let mut ed = Editor::new(src.as_bytes(), format).expect("editor");
3985 let blank = src.find("\n\n").expect("a blank line") + 1;
3986 assert!(
3987 matches!(
3988 ed.set_block(blank, BlockKind::Heading(1)),
3989 Err(Error::NotEditable)
3990 ),
3991 "{format:?}"
3992 );
3993 assert_eq!(ed.source().expect("source"), src.as_bytes(), "{format:?}");
3994 }
3995 }
3996
3997 #[test]
3998 fn task_items_report_their_checkbox_state() {
3999 for_both_formats("- [ ] a\n- [x] b\n- [X] c\n- d\n", |doc, format| {
4003 let nodes = doc.nodes().expect("nodes");
4004 let states: Vec<Option<bool>> = nodes
4005 .iter()
4006 .filter(|n| matches!(n.kind, Kind::TaskListItem | Kind::ListItem))
4007 .map(|n| n.checked)
4008 .collect();
4009 assert_eq!(
4010 states,
4011 vec![Some(false), Some(true), Some(true), None],
4012 "{format:?}"
4013 );
4014
4015 for n in nodes.iter().filter(|n| n.kind == Kind::Para) {
4018 assert_eq!(n.checked, None, "{format:?}");
4019 }
4020 });
4021 }
4022
4023 #[test]
4024 fn an_editor_reaches_the_caret_reads_through_its_document_view() {
4025 let mut ed = Editor::new("- a\n".as_bytes(), Format::Markdown).expect("editor");
4030 let mut view = ed.document().expect("document view");
4031
4032 assert_eq!(view.line_prefix(3).expect("prefix"), Some(0..2));
4033 let hit = view.node_at_caret(3).expect("hit").expect("some node");
4034 assert_eq!(hit.kind, Kind::Str);
4035 }
4036
4037 #[test]
4038 fn container_origin_is_none_for_non_containers() {
4039 let mut ed = Editor::new("# hi\n\npara\n".as_bytes(), Format::Markdown).expect("editor");
4042 for n in ed.nodes().expect("nodes") {
4043 assert_eq!(n.origin, None, "{} should carry no origin", n.kind);
4044 }
4045 }
4046
4047 #[test]
4048 fn flat_nodes_expose_directive_name_and_form() {
4049 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
4055 let mut ed = Editor::new_ext(
4056 src.as_bytes(),
4057 Format::Markdown,
4058 MarkdownExtensions {
4059 directives: true,
4060 ..Default::default()
4061 },
4062 )
4063 .expect("editor");
4064 let nodes = ed.nodes().expect("nodes");
4065
4066 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
4067 .iter()
4068 .filter(|n| n.kind == Kind::Container)
4069 .map(|n| (n.name.as_deref(), n.directive_form))
4070 .collect();
4071 assert_eq!(
4072 forms,
4073 vec![
4074 (Some("note"), Some(DirectiveForm::Container)),
4075 (Some("embed"), Some(DirectiveForm::Leaf)),
4076 (Some("abbr"), Some(DirectiveForm::Text)),
4077 ]
4078 );
4079
4080 let embed = nodes
4083 .iter()
4084 .find(|n| n.name.as_deref() == Some("embed"))
4085 .expect("embed");
4086 assert_eq!(
4087 embed.attrs,
4088 vec![("src".to_string(), Some("demo.html".to_string()))]
4089 );
4090 let para = nodes.iter().find(|n| n.kind == Kind::Para).expect("a para");
4091 assert!(para.directive_form.is_none() && para.name.is_none());
4092 }
4093
4094 #[test]
4095 fn editor_insert_child_and_delete() {
4096 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
4097 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4098 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
4099 ed.delete("0.1").expect("delete");
4100 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
4101 }
4102
4103 #[test]
4104 fn editor_edits_by_selector() {
4105 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4106 ed.replace("heading(\"Two\")", "## Renamed")
4107 .expect("replace");
4108 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
4109 }
4110
4111 #[test]
4112 fn editor_locator_errors_are_distinct() {
4113 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
4114 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
4115 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
4116 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
4117 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
4119 }
4120
4121 #[test]
4122 fn editor_reparse_break_rolls_back() {
4123 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
4124 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
4125 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
4126 }
4127
4128 #[test]
4129 fn editor_leaf_content_is_not_editable() {
4130 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4131 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
4132 }
4133
4134 #[test]
4135 fn editor_query_reflects_current_tree() {
4136 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
4137 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4138 assert_eq!(ed.query("element").expect("query").len(), 3);
4140 let json = ed.ast_json().expect("ast_json");
4141 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
4142 }
4143
4144 #[test]
4147 fn editor_edit_range_types_backspaces_and_reports_change() {
4148 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4149
4150 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
4152 assert_eq!(ed.source_str().unwrap(), "aXb\n");
4153 assert_eq!(c.old, 1..1);
4154 assert_eq!(c.new, 1..2);
4155 assert_eq!(c.delta(), 1);
4156
4157 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
4159 assert_eq!(ed.source_str().unwrap(), "ab\n");
4160 assert_eq!(c2.old, 1..2);
4161 assert_eq!(c2.new, 1..1);
4162 assert_eq!(c2.delta(), -1);
4163 }
4164
4165 #[test]
4166 fn editor_edit_range_rejects_bad_ranges() {
4167 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4168 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"); }
4172
4173 #[test]
4174 fn editor_last_change_reports_locator_ops_too() {
4175 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4176 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed")
4179 .expect("replace");
4180 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
4181 let c = ed.last_change().expect("a change was recorded");
4182 assert_eq!(c.old, 7..13);
4184 assert_eq!(c.new, 7..17);
4185 }
4186
4187 #[test]
4188 fn editor_nodes_is_a_walkable_flat_tree() {
4189 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4190 let nodes = ed.nodes().expect("nodes");
4191 assert!(!nodes.is_empty());
4192
4193 for (i, n) in nodes.iter().enumerate() {
4195 assert_eq!(n.id, NodeId(i as u32));
4196 }
4197 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
4199 assert_eq!(roots.len(), 1);
4200 assert_eq!(roots[0].kind, Kind::Doc);
4201
4202 let heading = nodes
4204 .iter()
4205 .find(|n| n.kind == Kind::Heading)
4206 .expect("a heading");
4207 assert_eq!(heading.level, Some(1));
4208 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
4209
4210 assert_eq!(heading.head, None);
4212 assert_eq!(heading.alignment, None);
4213
4214 for n in nodes.iter().filter(|n| n.parent.is_some()) {
4217 let p = &nodes[n.parent.unwrap().0 as usize];
4218 let mut kid = p.first_child;
4219 let mut seen = false;
4220 while let Some(NodeId(k)) = kid {
4221 if k == n.id.0 {
4222 seen = true;
4223 break;
4224 }
4225 kid = nodes[k as usize].next_sibling;
4226 }
4227 assert!(
4228 seen,
4229 "node {:?} not found among its parent's children",
4230 n.id
4231 );
4232 }
4233 }
4234
4235 #[test]
4236 fn editor_child_spans_and_subtree_agree_with_nodes() {
4237 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
4238 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4239 let all = ed.nodes().expect("nodes");
4240 let doc = all.iter().find(|n| n.kind == Kind::Doc).expect("doc");
4241
4242 let top = ed.child_spans(None).expect("child_spans");
4245 let mut want = Vec::new();
4246 let mut c = doc.first_child;
4247 while let Some(id) = c {
4248 want.push(id);
4249 c = all[id.0 as usize].next_sibling;
4250 }
4251 assert_eq!(top.len(), want.len(), "top-level count");
4252 for (m, id) in top.iter().zip(&want) {
4253 assert_eq!(m.node_id, id.0, "child id");
4254 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
4255 assert_eq!(m.span, all[id.0 as usize].span, "child span");
4256 }
4257 assert!(
4259 src[top[0].span.clone()].starts_with('#'),
4260 "first block is the heading"
4261 );
4262
4263 let list = top
4265 .iter()
4266 .find(|m| {
4267 matches!(
4268 m.kind,
4269 Kind::BulletList | Kind::OrderedList | Kind::TaskList
4270 )
4271 })
4272 .expect("a list");
4273 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
4274 assert_eq!(items.len(), 2);
4275 assert!(
4276 items.iter().all(|m| m.kind == Kind::ListItem),
4277 "items: {items:?}"
4278 );
4279
4280 let para = top
4282 .iter()
4283 .find(|m| m.kind == Kind::Para)
4284 .expect("a para")
4285 .node_id;
4286 let sub = ed.subtree(NodeId(para)).expect("subtree");
4287 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
4288 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
4289 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
4290 assert_eq!(sub[0].kind, Kind::Para);
4291 for (i, n) in sub.iter().enumerate() {
4292 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
4293 for link in [n.parent, n.first_child, n.next_sibling]
4294 .into_iter()
4295 .flatten()
4296 {
4297 assert!(
4298 (link.0 as usize) < sub.len(),
4299 "link {link:?} escapes the subtree"
4300 );
4301 }
4302 }
4303 assert!(
4304 src[sub[0].span.clone()].starts_with("Hello"),
4305 "absolute span: {:?}",
4306 &src[sub[0].span.clone()]
4307 );
4308
4309 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<Kind> {
4311 let mut out = Vec::new();
4312 let mut stack = vec![root];
4313 while let Some(id) = stack.pop() {
4314 let n = &all[id.0 as usize];
4315 out.push(n.kind.clone());
4316 let mut c = n.first_child;
4317 while let Some(cid) = c {
4318 stack.push(cid);
4319 c = all[cid.0 as usize].next_sibling;
4320 }
4321 }
4322 out
4323 }
4324 let mut want_kinds = arena_kinds(&all, NodeId(para));
4325 let mut got_kinds: Vec<Kind> = sub.iter().map(|n| n.kind.clone()).collect();
4326 want_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4330 got_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4331 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
4332
4333 assert!(matches!(
4335 ed.subtree(NodeId(9999)),
4336 Err(Error::InvalidArgument)
4337 ));
4338 }
4339
4340 #[test]
4341 fn flat_nodes_carry_table_head_and_alignment() {
4342 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
4346 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4347 let nodes = ed.nodes().expect("nodes");
4348
4349 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Row).collect();
4350 assert_eq!(rows.len(), 2, "a header row and one body row");
4351 assert_eq!(rows[0].head, Some(true), "first row is the header");
4352 assert_eq!(rows[1].head, Some(false), "second row is a body row");
4353
4354 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Cell).collect();
4355 assert_eq!(cells.len(), 4);
4356 assert_eq!(cells[0].alignment, Some(Alignment::Left));
4358 assert_eq!(cells[1].alignment, Some(Alignment::Right));
4359 assert_eq!(cells[2].alignment, Some(Alignment::Left));
4360 assert_eq!(cells[3].alignment, Some(Alignment::Right));
4361 assert_eq!(cells[0].head, Some(true));
4363 assert_eq!(cells[2].head, Some(false));
4364
4365 let mut plain =
4368 Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
4369 let pnodes = plain.nodes().expect("nodes");
4370 let pcell = pnodes
4371 .iter()
4372 .find(|n| n.kind == Kind::Cell)
4373 .expect("a cell");
4374 assert_eq!(pcell.alignment, Some(Alignment::Default));
4375 }
4376
4377 #[test]
4378 fn cell_extent_reports_merged_cells_and_nothing_else() {
4379 let src = "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td><td>b</td></tr></table>";
4380 let mut doc = Document::parse_str(src, Format::Html).expect("parse");
4381 let cells: Vec<NodeId> = doc
4382 .nodes()
4383 .expect("nodes")
4384 .iter()
4385 .filter(|n| n.kind == Kind::Cell)
4386 .map(|n| n.id)
4387 .collect();
4388 assert_eq!(cells.len(), 2);
4389 assert_eq!(doc.cell_extent(cells[0]).expect("extent"), Some((2, 3)));
4390 assert_eq!(doc.cell_extent(cells[1]).expect("extent"), Some((1, 1)));
4392
4393 let mut pipe =
4395 Document::parse_str("| a |\n| --- |\n| b |\n", Format::Markdown).expect("parse");
4396 let pipe_cell = pipe
4397 .nodes()
4398 .expect("nodes")
4399 .iter()
4400 .find(|n| n.kind == Kind::Cell)
4401 .expect("a cell")
4402 .id;
4403 assert_eq!(pipe.cell_extent(pipe_cell).expect("extent"), Some((1, 1)));
4404
4405 let root = NodeId(0);
4407 assert_eq!(pipe.cell_extent(root).expect("extent"), None);
4408 }
4409
4410 #[test]
4411 fn builder_add_cell_spanning_renders_colspan_and_rowspan() {
4412 let mut b = Builder::new().expect("builder");
4413 let wide_text = b.add_text(TextKind::Str, "wide").expect("str");
4414 let wide = b
4415 .add_cell_spanning(false, Alignment::Default, 2, 3)
4416 .expect("cell");
4417 b.set_children(wide, &[wide_text]).expect("children");
4418 let plain_text = b.add_text(TextKind::Str, "one").expect("str");
4419 let plain = b.add_cell(false, Alignment::Default).expect("cell");
4420 b.set_children(plain, &[plain_text]).expect("children");
4421 let row = b.add_row(false).expect("row");
4422 b.set_children(row, &[wide, plain]).expect("children");
4423 let table = b.add(VoidKind::Table).expect("table");
4424 b.set_children(table, &[row]).expect("children");
4425
4426 let html = String::from_utf8(b.render_html(table).expect("html")).expect("utf-8");
4427 assert!(
4428 html.contains("<td colspan=\"2\" rowspan=\"3\">wide</td>"),
4429 "{html}"
4430 );
4431 assert!(html.contains("<td>one</td>"), "{html}");
4433
4434 assert!(matches!(
4436 b.add_cell_spanning(false, Alignment::Default, 0, 1),
4437 Err(Error::InvalidArgument)
4438 ));
4439 }
4440
4441 #[test]
4442 fn editor_node_at_and_ancestors_hit_test_offsets() {
4443 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4444
4445 let m = ed
4447 .node_at(2)
4448 .expect("node_at")
4449 .expect("a node covers offset 2");
4450 assert!(m.span.contains(&2));
4451
4452 let chain = ed.ancestors_at(2).expect("ancestors_at");
4454 assert!(!chain.is_empty());
4455 assert_eq!(chain[0].kind, Kind::Doc);
4456 assert_eq!(chain.last().unwrap().node_id, m.node_id);
4457
4458 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
4460 }
4461
4462 #[test]
4465 fn editor_wrap_and_toggle_inline_round_trip() {
4466 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4467
4468 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
4470 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
4471 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
4472
4473 ed.toggle_inline(4, 8, InlineKind::Strong)
4475 .expect("toggle off");
4476 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4477
4478 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
4480 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
4481 }
4482
4483 #[test]
4484 fn editor_inline_kind_support_is_format_specific() {
4485 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4487 assert_eq!(
4488 md.wrap_range(2, 6, InlineKind::Mark),
4489 Err(Error::UnsupportedFormat)
4490 );
4491
4492 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4494 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
4495 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
4496 }
4497
4498 #[test]
4499 fn editor_toggle_strips_verbatim_via_content_span() {
4500 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
4501 ed.toggle_inline(2, 8, InlineKind::Verbatim)
4503 .expect("toggle code off");
4504 assert_eq!(ed.source_str().unwrap(), "a code b\n");
4505
4506 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
4509 ed2.toggle_inline(2, 7, InlineKind::Verbatim)
4510 .expect("toggle multi off");
4511 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
4512 }
4513
4514 #[test]
4515 fn editor_set_block_switches_para_and_heading_levels() {
4516 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
4517
4518 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
4520 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
4521
4522 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
4524 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
4525
4526 ed.set_block(2, BlockKind::Paragraph).expect("to para");
4528 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
4529 }
4530
4531 #[test]
4532 fn editor_set_block_rejects_bad_level_and_format() {
4533 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4534 assert_eq!(
4535 md.set_block(0, BlockKind::Heading(9)),
4536 Err(Error::InvalidArgument)
4537 );
4538
4539 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4540 assert_eq!(
4541 xml.set_block(1, BlockKind::Heading(1)),
4542 Err(Error::UnsupportedFormat)
4543 );
4544 }
4545
4546 #[test]
4547 fn editor_toggle_block_container_round_trips() {
4548 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
4549
4550 let c = ed
4551 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
4552 .expect("quote on");
4553 assert_eq!(ed.source_str().unwrap(), "> a\n");
4554 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
4555
4556 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4557 .expect("quote off");
4558 assert_eq!(ed.source_str().unwrap(), "a\n");
4559 }
4560
4561 #[test]
4562 fn editor_toggle_block_container_nests_a_partial_selection() {
4563 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
4564
4565 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4568 .expect("nest");
4569 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
4570
4571 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
4573 .expect("peel");
4574 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
4575 }
4576
4577 #[test]
4578 fn editor_toggle_block_container_numbers_and_converts_lists() {
4579 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
4580
4581 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
4583 .expect("ordered on");
4584 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
4585
4586 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
4588 .expect("convert");
4589 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
4590 }
4591
4592 #[test]
4593 fn editor_toggle_block_container_rejects_unspellable_format() {
4594 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4595 assert_eq!(
4596 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
4597 Err(Error::UnsupportedFormat)
4598 );
4599 }
4600
4601 #[test]
4602 fn editor_insert_link_wraps_and_repoints() {
4603 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4604
4605 ed.insert_link(2, 6, "http://x.dev").expect("link");
4606 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
4607
4608 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
4610 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
4611 }
4612
4613 #[test]
4614 fn editor_insert_link_repoints_an_autolink() {
4615 for format in [Format::Markdown, Format::Djot] {
4620 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
4621 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
4622 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
4623
4624 let nodes = ed.nodes().expect("nodes");
4626 let url = nodes
4627 .iter()
4628 .find(|n| n.kind == Kind::Url)
4629 .expect("still an autolink");
4630 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
4631 assert!(!nodes.iter().any(|n| n.kind == Kind::Link));
4632 }
4633 }
4634
4635 #[test]
4636 fn editor_insert_link_escapes_the_destination() {
4637 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4640 dj.insert_link(0, 1, "a)b").expect("link");
4641 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
4642
4643 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4647 md.insert_link(0, 1, "a b").expect("link");
4648 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
4649
4650 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
4651 dj2.insert_link(0, 1, "a b").expect("link");
4652 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
4653 }
4654
4655 #[test]
4656 fn editor_insert_image_escapes_the_destination_per_format() {
4657 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4660 md.insert_image(0, 1, "my cat.png").expect("image");
4661 assert_eq!(md.source_str().unwrap(), "\n");
4662
4663 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4664 dj.insert_image(0, 1, "my cat.png").expect("image");
4665 assert_eq!(dj.source_str().unwrap(), "\n");
4666
4667 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
4669 paren.insert_image(0, 1, "a)b.png").expect("image");
4670 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
4671 }
4672
4673 #[test]
4674 fn editor_insert_image_keeps_an_empty_alt_empty() {
4675 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4678 ed.insert_image(1, 1, "cat.png").expect("image");
4679 assert_eq!(ed.source_str().unwrap(), "ab\n");
4680 }
4681
4682 #[test]
4683 fn editor_insert_image_rejects_a_newline_destination() {
4684 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
4685 assert_eq!(
4686 ed.insert_image(0, 1, "a\nb.png"),
4687 Err(Error::InvalidArgument)
4688 );
4689
4690 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4691 assert_eq!(
4692 xml.insert_image(3, 5, "x.png"),
4693 Err(Error::UnsupportedFormat)
4694 );
4695 }
4696
4697 #[test]
4698 fn editor_insert_link_rejects_a_newline_destination() {
4699 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
4700 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
4701
4702 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4703 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
4704 }
4705
4706 #[test]
4707 fn editor_insert_literal_keeps_typed_specials_literal() {
4708 for format in [Format::Markdown, Format::Djot] {
4709 let mut ed = Editor::new_str("z\n", format).expect("editor");
4710 ed.insert_literal(0, "*hi*").expect("literal");
4712
4713 let nodes = ed.nodes().expect("nodes");
4715 assert!(
4716 !nodes
4717 .iter()
4718 .any(|n| n.kind == Kind::Emph || n.kind == Kind::Strong)
4719 );
4720 let text: String = nodes
4721 .iter()
4722 .filter(|n| n.kind == Kind::Str)
4723 .filter_map(|n| n.text.clone())
4724 .collect();
4725 assert_eq!(text, "*hi*z");
4726 }
4727 }
4728
4729 #[test]
4730 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
4731 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
4733 ed.insert_literal(1, "# ").expect("literal");
4734 assert_eq!(ed.source_str().unwrap(), "a# z\n");
4735
4736 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
4738 ed2.insert_literal(0, "# ").expect("literal");
4739 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
4740 assert!(
4741 !ed2.nodes()
4742 .expect("nodes")
4743 .iter()
4744 .any(|n| n.kind == Kind::Heading)
4745 );
4746 }
4747
4748 #[test]
4749 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
4750 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4751 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
4752
4753 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4754 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
4755 }
4756
4757 #[test]
4758 fn editor_insert_line_break_splices_in_cell_br() {
4759 let mut ed =
4760 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
4761 ed.insert_line_break(3).expect("line break");
4763 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
4764 let nodes = ed.nodes().expect("nodes");
4766 assert!(nodes.iter().any(|n| n.kind == Kind::HardBreak));
4767 assert!(!nodes.iter().any(|n| n.kind == Kind::RawInline));
4768 }
4769
4770 #[test]
4771 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
4772 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
4774 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
4775
4776 let mut dj = Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
4778 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
4779
4780 let mut ed =
4782 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
4783 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
4784 }
4785
4786 #[test]
4787 fn editor_insert_thematic_break_is_blank_separated_per_format() {
4788 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
4792 md.insert_thematic_break(0).expect("rule");
4793 assert_eq!(md.source_str().unwrap(), "a\n\n---\n");
4794 let nodes = md.nodes().expect("nodes");
4795 assert!(nodes.iter().any(|n| n.kind == Kind::ThematicBreak));
4796 assert!(!nodes.iter().any(|n| n.kind == Kind::Heading));
4797
4798 let mut dj = Editor::new_str("a\n", Format::Djot).expect("editor");
4801 dj.insert_thematic_break(0).expect("rule");
4802 assert_eq!(dj.source_str().unwrap(), "a\n\n* * *\n");
4803
4804 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4805 assert_eq!(xml.insert_thematic_break(3), Err(Error::UnsupportedFormat));
4806 }
4807
4808 #[test]
4809 fn editor_split_block_keeps_both_halves_the_same_kind() {
4810 let mut item = Editor::new_str("- this is a list item\n", Format::Markdown).expect("editor");
4813 item.split_block(10).expect("split");
4814 assert_eq!(item.source_str().unwrap(), "- this is \n- a list item\n");
4815 let nodes = item.nodes().expect("nodes");
4816 assert_eq!(nodes.iter().filter(|n| n.kind == Kind::ListItem).count(), 2);
4817
4818 let mut tail = Editor::new_str("- a\n", Format::Markdown).expect("editor");
4820 tail.split_block(3).expect("split");
4821 assert_eq!(tail.source_str().unwrap(), "- a\n- \n");
4822
4823 let mut para = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4825 para.split_block(1).expect("split");
4826 assert_eq!(para.source_str().unwrap(), "a\n\nb\n");
4827
4828 let mut table =
4830 Editor::new_str("| a | b |\n|---|---|\n| c | d |\n", Format::Markdown).expect("editor");
4831 assert_eq!(table.split_block(3), Err(Error::NotEditable));
4832
4833 let mut empty = Editor::new_str("", Format::Markdown).expect("editor");
4834 assert_eq!(empty.split_block(0), Err(Error::NotFound));
4835 }
4836
4837 #[test]
4838 fn editor_toggle_code_block_round_trips_and_measures_the_fence() {
4839 let mut ed = Editor::new_str("a\n", Format::Markdown).expect("editor");
4840 ed.toggle_code_block(0, 1, Some("zig")).expect("fence");
4841 assert_eq!(ed.source_str().unwrap(), "```zig\na\n```\n");
4842 let nodes = ed.nodes().expect("nodes");
4843 assert!(nodes.iter().any(|n| n.kind == Kind::CodeBlock));
4844
4845 ed.toggle_code_block(0, 0, None).expect("unfence");
4846 assert_eq!(ed.source_str().unwrap(), "a\n");
4847
4848 let mut runs = Editor::new_str("a ``` b\n", Format::Markdown).expect("editor");
4851 runs.toggle_code_block(0, 7, None).expect("fence");
4852 assert_eq!(runs.source_str().unwrap(), "````\na ``` b\n````\n");
4853 }
4854
4855 #[test]
4856 fn editor_toggle_code_block_refuses_inside_a_list_item() {
4857 let mut ed = Editor::new_str("- a\n- b\n", Format::Markdown).expect("editor");
4860 assert_eq!(ed.toggle_code_block(2, 3, None), Err(Error::NotEditable));
4861 assert_eq!(ed.source_str().unwrap(), "- a\n- b\n");
4862 }
4863
4864 #[test]
4865 fn editor_set_code_language_retags_clears_and_refuses() {
4866 let mut ed = Editor::new_str("```zig\na\n```\n", Format::Markdown).expect("editor");
4867 ed.set_code_language(0, Some("rust")).expect("retag");
4868 assert_eq!(ed.source_str().unwrap(), "```rust\na\n```\n");
4869
4870 ed.set_code_language(0, None).expect("clear");
4873 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
4874 ed.set_code_language(0, Some("")).expect("empty");
4875 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
4876
4877 assert_eq!(
4880 ed.set_code_language(0, Some("a b")),
4881 Err(Error::InvalidArgument)
4882 );
4883 let mut dj = Editor::new_str("```\na\n```\n", Format::Djot).expect("editor");
4885 dj.set_code_language(0, Some("a b"))
4886 .expect("djot info string");
4887 assert_eq!(dj.source_str().unwrap(), "```a b\na\n```\n");
4888
4889 let mut para = Editor::new_str("x\n", Format::Markdown).expect("editor");
4890 assert_eq!(para.set_code_language(0, Some("zig")), Err(Error::NotFound));
4891 }
4892
4893 #[test]
4894 fn editor_task_checkbox_gestures() {
4895 let mut ed = Editor::new_str("- a\n", Format::Markdown).expect("editor");
4896
4897 ed.toggle_task_item(2).expect("add box");
4900 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
4901 assert!(
4902 ed.nodes()
4903 .unwrap()
4904 .iter()
4905 .any(|n| n.kind == Kind::TaskListItem)
4906 );
4907
4908 ed.set_task_checked(6, true).expect("tick");
4909 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
4910 ed.set_task_checked(6, true).expect("no-op");
4912 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
4913
4914 ed.toggle_task_checked(6).expect("flip");
4915 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
4916
4917 ed.toggle_task_item(6).expect("remove box");
4918 assert_eq!(ed.source_str().unwrap(), "- a\n");
4919
4920 assert_eq!(ed.set_task_checked(2, true), Err(Error::NotEditable));
4923 let mut para = Editor::new_str("a\n", Format::Markdown).expect("editor");
4925 assert_eq!(para.toggle_task_item(0), Err(Error::NotFound));
4926 }
4927
4928 #[test]
4929 fn editor_insert_footnote_writes_both_halves_as_one_edit() {
4930 for format in [Format::Markdown, Format::Djot] {
4931 let mut ed = Editor::new_str("see\n", format).expect("editor");
4932 ed.insert_footnote(3, "a").expect("footnote");
4933 assert_eq!(ed.source_str().unwrap(), "see[^a]\n\n[^a]: \n");
4934
4935 let nodes = ed.nodes().expect("nodes");
4937 assert!(nodes.iter().any(|n| n.kind == Kind::FootnoteReference));
4938 assert!(nodes.iter().any(|n| n.kind == Kind::Footnote));
4939
4940 ed.undo().expect("undo");
4942 assert_eq!(ed.source_str().unwrap(), "see\n");
4943 }
4944 }
4945
4946 #[test]
4947 fn editor_insert_footnote_reuses_an_existing_definition() {
4948 let mut ed = Editor::new_str("see\n", Format::Markdown).expect("editor");
4949 ed.insert_footnote(3, "a").expect("first");
4950 ed.insert_footnote(7, "a").expect("second reference");
4951 assert_eq!(ed.source_str().unwrap(), "see[^a][^a]\n\n[^a]: \n");
4952 let defs = ed
4953 .nodes()
4954 .unwrap()
4955 .iter()
4956 .filter(|n| n.kind == Kind::Footnote)
4957 .count();
4958 assert_eq!(defs, 1);
4959
4960 assert_eq!(ed.insert_footnote(3, ""), Err(Error::InvalidArgument));
4961 assert_eq!(ed.insert_footnote(3, "a]b"), Err(Error::InvalidArgument));
4962 }
4963
4964 #[test]
4965 fn editor_undo_redo_round_trip() {
4966 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
4967 ed.edit_range(5, 5, "!").expect("edit");
4968 assert_eq!(ed.source_str().unwrap(), "hello!\n");
4969
4970 let change = ed.undo().expect("undo ok").expect("something to undo");
4971 assert_eq!(ed.source_str().unwrap(), "hello\n");
4972 assert_eq!(change.new.end, 5);
4973 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
4974
4975 ed.redo().expect("redo ok").expect("something to redo");
4976 assert_eq!(ed.source_str().unwrap(), "hello!\n");
4977 }
4978
4979 #[test]
4980 fn editor_coalesce_folds_a_run() {
4981 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
4982 ed.edit_range(0, 0, "a").expect("edit");
4983 ed.edit_range(1, 1, "b").expect("edit");
4984 ed.coalesce_last_undo().expect("coalesce");
4985 assert_eq!(ed.source_str().unwrap(), "ab\n");
4986 ed.undo().expect("undo ok").expect("something to undo");
4988 assert_eq!(ed.source_str().unwrap(), "\n");
4989 assert!(ed.undo().expect("undo ok").is_none());
4990 }
4991
4992 #[test]
4993 fn editor_revision_bumps_per_successful_mutation() {
4994 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
4995 assert_eq!(ed.revision(), 0);
4996 ed.edit_range(1, 1, "y").expect("edit");
4997 assert_eq!(ed.revision(), 1);
4998
4999 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
5001 assert_eq!(xml.revision(), 0);
5002 assert!(xml.replace_content("0", "<b>").is_err());
5003 assert_eq!(xml.revision(), 0);
5004
5005 ed.undo().expect("undo ok").expect("something to undo");
5007 assert_eq!(ed.revision(), 2);
5008 ed.redo().expect("redo ok").expect("something to redo");
5009 assert_eq!(ed.revision(), 3);
5010 }
5011
5012 #[test]
5013 fn editor_dirty_range_tracks_and_clears() {
5014 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
5015 assert_eq!(ed.dirty_range(), None);
5017
5018 ed.edit_range(2, 2, "XY").expect("edit");
5020 assert_eq!(ed.dirty_range(), Some(2..4));
5021
5022 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
5026 assert!(
5027 d.start <= 2 && d.end >= 10,
5028 "range {d:?} must cover both edits"
5029 );
5030
5031 let rev = ed.revision();
5033 ed.clear_dirty();
5034 assert_eq!(ed.dirty_range(), None);
5035 assert_eq!(ed.revision(), rev);
5036
5037 ed.undo().expect("undo ok").expect("something to undo");
5039 assert!(ed.dirty_range().is_some());
5040 }
5041
5042 #[test]
5043 fn editor_caret_blob_follows_undo_and_redo() {
5044 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5045 assert!(ed.caret_blob().unwrap().is_empty());
5046
5047 ed.set_caret_blob(b"before").expect("set caret");
5049 ed.edit_range(5, 5, "!").expect("edit");
5050 assert!(ed.caret_blob().unwrap().is_empty());
5052 ed.set_caret_blob(b"after").expect("set caret");
5053
5054 ed.undo().expect("undo ok").expect("something to undo");
5056 assert_eq!(ed.source_str().unwrap(), "hello\n");
5057 assert_eq!(ed.caret_blob().unwrap(), b"before");
5058
5059 ed.redo().expect("redo ok").expect("something to redo");
5061 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5062 assert_eq!(ed.caret_blob().unwrap(), b"after");
5063 }
5064
5065 #[test]
5066 fn editor_coalesced_run_keeps_the_pre_run_caret() {
5067 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5068 ed.set_caret_blob(b"c0").expect("set caret");
5069 ed.edit_range(0, 0, "a").expect("edit");
5070 ed.set_caret_blob(b"c1").expect("set caret");
5071 ed.edit_range(1, 1, "b").expect("edit");
5072 ed.coalesce_last_undo().expect("coalesce");
5073 ed.set_caret_blob(b"c2").expect("set caret");
5074
5075 ed.undo().expect("undo ok").expect("something to undo");
5077 assert_eq!(ed.source_str().unwrap(), "\n");
5078 assert_eq!(ed.caret_blob().unwrap(), b"c0");
5079 }
5080
5081 #[test]
5082 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
5083 let mut ed = Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
5084 ed.renumber_ordered_lists(0).expect("renumber ok");
5085 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
5086 }
5087
5088 #[test]
5089 fn editor_renumber_ordered_lists_leaves_djot_prose_alone() {
5090 let src = "1. a\n 2. b\n2. c\n";
5093 let mut dj = Editor::new_str(src, Format::Djot).expect("editor");
5094 dj.renumber_ordered_lists(0).expect("renumber ok");
5095 assert_eq!(dj.source_str().unwrap(), src);
5096
5097 let mut md = Editor::new_str(src, Format::Markdown).expect("editor");
5098 md.renumber_ordered_lists(0).expect("renumber ok");
5099 assert_eq!(md.source_str().unwrap(), "1. a\n 1. b\n2. c\n");
5100 }
5101
5102 #[test]
5103 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
5104 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
5105 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
5106 }
5107
5108 #[test]
5109 fn editor_table_insert_row_and_set_alignment() {
5110 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
5111 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
5112 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
5114 ed.source_str().unwrap(),
5115 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
5116 );
5117 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
5119 }
5120
5121 #[test]
5122 fn editor_table_edit_off_a_table_is_not_found() {
5123 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
5124 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
5125 }
5126
5127 #[test]
5128 fn editor_set_block_converts_setext_heading() {
5129 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
5131 ed.set_block(0, BlockKind::Heading(1))
5132 .expect("setext to atx");
5133 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
5134 }
5135
5136 #[test]
5137 fn editor_unwrap_and_smart_delete() {
5138 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
5139 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
5141
5142 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
5143 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
5145 }
5146
5147 #[test]
5148 fn editor_directives_require_the_extension_flag() {
5149 let src = ":::vis{.public}\nhi\n:::\n";
5150 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
5153 assert_eq!(plain.query("directive").expect("query").len(), 0);
5154 let mut ext = Editor::new_ext(
5156 src.as_bytes(),
5157 Format::Markdown,
5158 MarkdownExtensions {
5159 directives: true,
5160 ..Default::default()
5161 },
5162 )
5163 .expect("editor");
5164 assert_eq!(ext.query("directive").expect("query").len(), 1);
5165 }
5166
5167 #[test]
5168 fn document_html_elements_make_embedded_img_queryable() {
5169 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
5170 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
5172 assert_eq!(plain.query("image").expect("query").len(), 0);
5173 let mut ext = Document::parse_str_with(
5175 src,
5176 Format::Markdown,
5177 MarkdownExtensions {
5178 html_elements: true,
5179 ..Default::default()
5180 },
5181 )
5182 .expect("parse");
5183 let images = ext.query("image").expect("query");
5184 assert_eq!(images.len(), 1);
5185 assert_eq!(images[0].kind, Kind::Image);
5186 }
5187
5188 #[test]
5189 fn editor_filter_public_audience_view() {
5190 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
5191 let mut ed = Editor::new_ext(
5192 src.as_bytes(),
5193 Format::Markdown,
5194 MarkdownExtensions {
5195 directives: true,
5196 ..Default::default()
5197 },
5198 )
5199 .expect("editor");
5200 ed.filter(
5202 "directive[name=vis]",
5203 Some("directive[class~=public]"),
5204 true,
5205 )
5206 .expect("filter");
5207 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
5208 }
5209
5210 #[test]
5211 fn editor_filter_rejects_a_malformed_selector() {
5212 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
5213 assert_eq!(
5214 ed.filter("list >", None, false),
5215 Err(Error::InvalidArgument)
5216 );
5217 }
5218
5219 #[test]
5220 fn builder_builds_and_renders_a_document() {
5221 let mut b = Builder::new().expect("builder");
5222
5223 let title = b.add_text(TextKind::Str, "Title").unwrap();
5225 let heading = b.add_heading(1).unwrap();
5226 b.set_children(heading, &[title]).unwrap();
5227
5228 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
5229 let world = b.add_text(TextKind::Str, "world").unwrap();
5230 let emph = b.add(VoidKind::Emph).unwrap();
5231 b.set_children(emph, &[world]).unwrap();
5232 let para = b.add(VoidKind::Para).unwrap();
5233 b.set_children(para, &[hello, emph]).unwrap();
5234
5235 let doc = b.add(VoidKind::Doc).unwrap();
5236 b.set_children(doc, &[heading, para]).unwrap();
5237
5238 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
5239 assert!(html.contains("<h1>Title</h1>"), "{html}");
5240 assert!(html.contains("<em>world</em>"), "{html}");
5241
5242 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5243 assert!(md.contains("# Title"), "{md}");
5244 assert!(md.contains("*world*"), "{md}");
5245
5246 let matches = b.query(doc, "heading").unwrap();
5247 assert_eq!(matches.len(), 1);
5248 assert_eq!(matches[0].kind, Kind::Heading);
5249
5250 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
5251 assert!(json.contains("\"kind\": \"doc\""), "{json}");
5252 }
5253
5254 #[test]
5255 fn builder_element_with_attributes() {
5256 let mut b = Builder::new().expect("builder");
5257 let inner = b.add_text(TextKind::Str, "hi").unwrap();
5258 let el = b.add_element("section").unwrap();
5259 b.set_children(el, &[inner]).unwrap();
5260 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)])
5261 .unwrap();
5262
5263 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
5264 assert!(html.contains("<section"), "{html}");
5265 assert!(html.contains("class=\"note\""), "{html}");
5266 assert!(html.contains("hidden"), "{html}");
5267 }
5268
5269 #[test]
5270 fn builder_lists_round_trip_to_markdown() {
5271 let mut b = Builder::new().expect("builder");
5272
5273 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
5275 let one_para = b.add(VoidKind::Para).unwrap();
5276 b.set_children(one_para, &[one_txt]).unwrap();
5277 let one = b.add(VoidKind::ListItem).unwrap();
5278 b.set_children(one, &[one_para]).unwrap();
5279
5280 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
5281 let two_para = b.add(VoidKind::Para).unwrap();
5282 b.set_children(two_para, &[two_txt]).unwrap();
5283 let two = b.add(VoidKind::ListItem).unwrap();
5284 b.set_children(two, &[two_para]).unwrap();
5285
5286 let list = b
5287 .add_ordered_list(
5288 OrderedNumbering::Decimal,
5289 OrderedDelim::Period,
5290 true,
5291 Some(1),
5292 )
5293 .unwrap();
5294 b.set_children(list, &[one, two]).unwrap();
5295 let doc = b.add(VoidKind::Doc).unwrap();
5296 b.set_children(doc, &[list]).unwrap();
5297
5298 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5299 assert!(md.contains("1. one"), "{md}");
5300 assert!(md.contains("2. two"), "{md}");
5301 }
5302
5303 #[test]
5304 fn builder_rejects_invalid_kind_and_id() {
5305 let b = Builder::new().expect("builder");
5306 let mut id = 0u32;
5310 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
5311 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5312
5313 let mut ptr = std::ptr::null();
5315 let mut len = 0usize;
5316 let status =
5317 unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
5318 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5319 }
5320
5321 fn all_gestures() -> Vec<Gesture> {
5325 let inline = [
5326 InlineKind::Strong,
5327 InlineKind::Emph,
5328 InlineKind::Verbatim,
5329 InlineKind::Mark,
5330 InlineKind::Superscript,
5331 InlineKind::Subscript,
5332 InlineKind::Insert,
5333 InlineKind::Delete,
5334 ];
5335 let mut all: Vec<Gesture> = Vec::new();
5336 for k in inline {
5337 all.push(Gesture::WrapRange(k));
5338 all.push(Gesture::ToggleInline(k));
5339 }
5340 for k in [
5341 BlockContainerKind::BlockQuote,
5342 BlockContainerKind::BulletList,
5343 BlockContainerKind::OrderedList,
5344 ] {
5345 all.push(Gesture::ToggleBlockContainer(k));
5346 }
5347 all.extend([
5348 Gesture::SetBlock,
5349 Gesture::InsertThematicBreak,
5350 Gesture::ToggleCodeBlock,
5351 Gesture::SetCodeLanguage,
5352 Gesture::ToggleTaskItem,
5353 Gesture::SetTaskChecked,
5354 Gesture::ToggleTaskChecked,
5355 Gesture::InsertLink,
5356 Gesture::InsertImage,
5357 Gesture::InsertFootnote,
5358 Gesture::InsertLiteral,
5359 Gesture::InsertLineBreak,
5360 ]);
5361 all
5362 }
5363
5364 #[test]
5365 fn supports_answers_per_gesture_where_authorable_cannot() {
5366 assert!(Format::Html.is_authorable());
5371 assert!(Format::Html.supports(Gesture::ToggleInline(InlineKind::Strong)));
5372 assert!(!Format::Html.supports(Gesture::SetBlock));
5373 assert!(!Format::Html.supports(Gesture::ToggleBlockContainer(
5374 BlockContainerKind::BlockQuote
5375 )));
5376 assert!(!Format::Html.supports(Gesture::ToggleCodeBlock));
5377 assert!(!Format::Html.supports(Gesture::InsertLiteral));
5378
5379 for fmt in [Format::Xml, Format::Asciidoc] {
5382 assert!(!fmt.is_authorable());
5383 for g in all_gestures() {
5384 assert!(!fmt.supports(g), "{fmt:?} claims to spell {g:?}");
5385 }
5386 }
5387
5388 assert!(Format::Djot.supports(Gesture::ToggleInline(InlineKind::Mark)));
5391 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
5392 assert!(Format::Markdown.supports(Gesture::InsertLineBreak));
5393 assert!(!Format::Djot.supports(Gesture::InsertLineBreak));
5394 }
5395
5396 #[test]
5397 fn supports_agrees_with_what_the_editor_then_does() {
5398 for fmt in [Format::Djot, Format::Markdown, Format::Html] {
5403 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5404 let claimed = fmt.supports(Gesture::ToggleInline(InlineKind::Mark));
5405 let observed = ed.toggle_inline(0, 2, InlineKind::Mark);
5406 assert_eq!(
5407 claimed,
5408 !matches!(observed, Err(Error::UnsupportedFormat)),
5409 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5410 );
5411
5412 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5413 let claimed = fmt.supports(Gesture::SetBlock);
5414 let observed = ed.set_block(0, BlockKind::Heading(1));
5415 assert_eq!(
5416 claimed,
5417 !matches!(observed, Err(Error::UnsupportedFormat)),
5418 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5419 );
5420 }
5421 }
5422
5423 #[test]
5424 fn supports_rides_the_gestures_own_kind_space() {
5425 let (g, k) = Gesture::ToggleBlockContainer(BlockContainerKind::BulletList).to_c();
5430 assert_eq!((g, k), (3, 1));
5431 let (g, k) = Gesture::ToggleInline(InlineKind::Emph).to_c();
5432 assert_eq!((g, k), (1, 1));
5433 assert_eq!(Gesture::InsertLink.to_c(), (10, 0));
5436
5437 let mut out: c_int = 0;
5439 let status = unsafe {
5440 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 10, 3, &mut out)
5441 };
5442 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5443 let status = unsafe {
5444 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 9999, 0, &mut out)
5445 };
5446 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5447 }
5448}