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 line_prefix(&mut self, offset: usize) -> Result<Option<Range<usize>>, Error> {
961 let mut span = ffi::TwigSpan { start: 0, end: 0 };
962 let status =
963 unsafe { ffi::twig_document_line_prefix(self.raw.as_ptr(), offset, &mut span) };
964 match status.0 {
965 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
966 ffi::TwigStatus::NOT_FOUND => Ok(None),
967 _ => Err(Error::from_status(status).unwrap_err()),
968 }
969 }
970
971 pub fn continuation_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
997 self.prefix_via(offset, ffi::twig_document_continuation_prefix)
998 }
999
1000 pub fn blank_line_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1012 self.prefix_via(offset, ffi::twig_document_blank_line_prefix)
1013 }
1014
1015 fn prefix_via(
1017 &mut self,
1018 offset: usize,
1019 f: unsafe extern "C" fn(
1020 *mut ffi::TwigDocument,
1021 usize,
1022 *mut *const u8,
1023 *mut usize,
1024 *mut usize,
1025 ) -> ffi::TwigStatus,
1026 ) -> Result<LinePrefix, Error> {
1027 let mut ptr: *const u8 = std::ptr::null();
1028 let mut len = 0usize;
1029 let mut columns = 0usize;
1030 let status = unsafe { f(self.raw.as_ptr(), offset, &mut ptr, &mut len, &mut columns) };
1031 Error::from_status(status)?;
1032 let text = if ptr.is_null() || len == 0 {
1033 String::new()
1034 } else {
1035 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1036 String::from_utf8(bytes.to_vec()).map_err(|_| Error::Internal)?
1037 };
1038 Ok(LinePrefix { text, columns })
1039 }
1040
1041 pub fn cell_extent(&mut self, node: NodeId) -> Result<Option<(u32, u32)>, Error> {
1053 let raw = self.raw.as_ptr();
1054 let mut colspan: u32 = 0;
1055 let status = unsafe { ffi::twig_document_cell_colspan(raw, node.0, &mut colspan) };
1056 match status.0 {
1057 ffi::TwigStatus::OK => {}
1058 ffi::TwigStatus::NOT_FOUND => return Ok(None),
1059 _ => return Err(Error::from_status(status).unwrap_err()),
1060 }
1061 let mut rowspan: u32 = 0;
1062 Error::from_status(unsafe { ffi::twig_document_cell_rowspan(raw, node.0, &mut rowspan) })?;
1063 Ok(Some((colspan, rowspan)))
1064 }
1065
1066 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1071 let raw = self.raw.as_ptr();
1072 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_nodes(raw, ptr, len) })
1073 }
1074
1075 pub fn definitions(&mut self) -> Result<Vec<QueryMatch>, Error> {
1092 let raw = self.raw.as_ptr();
1093 collect_matches(|ptr, len| unsafe { ffi::twig_document_definitions(raw, ptr, len) })
1094 }
1095
1096 pub fn diagnostics(&mut self, target: Target) -> Result<Vec<Warning>, Error> {
1116 let raw = self.raw.as_ptr();
1117 let code = ffi::TwigFormat::from(target) as c_int;
1118 let mut ptr: *const ffi::TwigWarning = std::ptr::null();
1119 let mut len = 0usize;
1120 let status = unsafe { ffi::twig_document_diagnostics(raw, code, &mut ptr, &mut len) };
1121 Error::from_status(status)?;
1122 if len == 0 || ptr.is_null() {
1123 return Ok(Vec::new());
1124 }
1125 let raw_warnings = unsafe { std::slice::from_raw_parts(ptr, len) };
1126 Ok(raw_warnings
1127 .iter()
1128 .map(|w| Warning {
1129 fidelity: Fidelity::from_c(w.fidelity),
1130 path: borrowed_bytes(w.path_ptr, w.path_len).unwrap_or_default(),
1131 kind: Kind::from(borrowed_cstr(w.kind).unwrap_or_default().as_str()),
1132 })
1133 .collect())
1134 }
1135
1136 pub fn children(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1142 let raw = self.raw.as_ptr();
1143 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1144 collect_matches(|ptr, len| unsafe { ffi::twig_document_children(raw, id, ptr, len) })
1145 }
1146
1147 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1153 let raw = self.raw.as_ptr();
1154 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_subtree(raw, node.0, ptr, len) })
1155 }
1156
1157 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1162 let mut m = empty_ffi_match();
1163 let status = unsafe { ffi::twig_document_node_at(self.raw.as_ptr(), offset, &mut m) };
1164 match status.0 {
1165 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1166 ffi::TwigStatus::NOT_FOUND => Ok(None),
1167 _ => Err(Error::from_status(status).unwrap_err()),
1168 }
1169 }
1170
1171 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1175 let raw = self.raw.as_ptr();
1176 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1177 let mut len = 0usize;
1178 let status = unsafe { ffi::twig_document_nodes_at(raw, offset, &mut ptr, &mut len) };
1179 match status.0 {
1180 ffi::TwigStatus::OK => {}
1181 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1182 _ => return Err(Error::from_status(status).unwrap_err()),
1183 }
1184 if len == 0 || ptr.is_null() {
1185 return Ok(Vec::new());
1186 }
1187 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1188 raw_matches.iter().map(query_match_from_ffi).collect()
1189 }
1190
1191 pub fn node_at_caret(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1214 let mut m = empty_ffi_match();
1215 let status = unsafe { ffi::twig_document_node_at_caret(self.raw.as_ptr(), offset, &mut m) };
1216 match status.0 {
1217 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1218 ffi::TwigStatus::NOT_FOUND => Ok(None),
1219 _ => Err(Error::from_status(status).unwrap_err()),
1220 }
1221 }
1222
1223 pub fn ancestors_at_caret(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1227 let raw = self.raw.as_ptr();
1228 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1229 let mut len = 0usize;
1230 let status = unsafe { ffi::twig_document_nodes_at_caret(raw, offset, &mut ptr, &mut len) };
1231 match status.0 {
1232 ffi::TwigStatus::OK => {}
1233 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1234 _ => return Err(Error::from_status(status).unwrap_err()),
1235 }
1236 if len == 0 || ptr.is_null() {
1237 return Ok(Vec::new());
1238 }
1239 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1240 raw_matches.iter().map(query_match_from_ffi).collect()
1241 }
1242}
1243
1244#[derive(Debug)]
1255pub struct DocumentView<'a> {
1256 doc: Document,
1257 _editor: PhantomData<&'a mut Editor>,
1258}
1259
1260impl std::ops::Deref for DocumentView<'_> {
1261 type Target = Document;
1262
1263 fn deref(&self) -> &Document {
1264 &self.doc
1265 }
1266}
1267
1268impl std::ops::DerefMut for DocumentView<'_> {
1269 fn deref_mut(&mut self) -> &mut Document {
1270 &mut self.doc
1271 }
1272}
1273
1274impl Drop for Document {
1275 fn drop(&mut self) {
1276 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
1277 }
1278}
1279
1280#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1286pub struct MarkdownExtensions {
1287 pub directives: bool,
1289 pub math: bool,
1291 pub html_elements: bool,
1296}
1297
1298impl MarkdownExtensions {
1299 fn to_flags(self) -> u32 {
1300 let mut flags = 0;
1301 if self.directives {
1302 flags |= ffi::TWIG_MD_DIRECTIVES;
1303 }
1304 if self.math {
1305 flags |= ffi::TWIG_MD_MATH;
1306 }
1307 if self.html_elements {
1308 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
1309 }
1310 flags
1311 }
1312}
1313
1314#[derive(Debug)]
1320pub struct Editor {
1321 raw: NonNull<ffi::TwigEditor>,
1322}
1323
1324impl Editor {
1325 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
1328 let mut raw = std::ptr::null_mut();
1329 let ffi_format: ffi::TwigFormat = format.into();
1330 let status = unsafe {
1331 ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw)
1332 };
1333 Error::from_status(status)?;
1334 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1335 Ok(Self { raw })
1336 }
1337
1338 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
1339 Self::new(input.as_bytes(), format)
1340 }
1341
1342 pub fn new_ext(
1347 input: &[u8],
1348 format: Format,
1349 extensions: MarkdownExtensions,
1350 ) -> Result<Self, Error> {
1351 let mut raw = std::ptr::null_mut();
1352 let ffi_format: ffi::TwigFormat = format.into();
1353 let status = unsafe {
1354 ffi::twig_editor_create_ext(
1355 input.as_ptr(),
1356 input.len(),
1357 ffi_format as i32,
1358 extensions.to_flags(),
1359 &mut raw,
1360 )
1361 };
1362 Error::from_status(status)?;
1363 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1364 Ok(Self { raw })
1365 }
1366
1367 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1369 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1370 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
1371 })
1372 }
1373
1374 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1377 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1378 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
1379 })
1380 }
1381
1382 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1384 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1385 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
1386 })
1387 }
1388
1389 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1391 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1392 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
1393 })
1394 }
1395
1396 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
1399 let status = unsafe {
1400 ffi::twig_editor_insert_child(
1401 self.raw.as_ptr(),
1402 locator.as_ptr(),
1403 locator.len(),
1404 index,
1405 text.as_ptr(),
1406 text.len(),
1407 )
1408 };
1409 Error::from_status(status)
1410 }
1411
1412 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
1415 let status =
1416 unsafe { ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1417 Error::from_status(status)
1418 }
1419
1420 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
1423 let status = unsafe {
1424 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
1425 };
1426 Error::from_status(status)
1427 }
1428
1429 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
1433 let status =
1434 unsafe { ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1435 Error::from_status(status)
1436 }
1437
1438 pub fn filter(
1443 &mut self,
1444 drop: &str,
1445 keep: Option<&str>,
1446 unwrap_kept: bool,
1447 ) -> Result<(), Error> {
1448 let (keep_ptr, keep_len) = match keep {
1449 Some(k) => (k.as_ptr(), k.len()),
1450 None => (std::ptr::null(), 0),
1451 };
1452 let status = unsafe {
1453 ffi::twig_editor_filter(
1454 self.raw.as_ptr(),
1455 drop.as_ptr(),
1456 drop.len(),
1457 keep_ptr,
1458 keep_len,
1459 unwrap_kept as i32,
1460 )
1461 };
1462 Error::from_status(status)
1463 }
1464
1465 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
1467 let raw = self.raw.as_ptr();
1468 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
1469 }
1470
1471 pub fn source_str(&mut self) -> Result<String, Error> {
1473 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
1474 }
1475
1476 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
1479 let raw = self.raw.as_ptr();
1480 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
1481 }
1482
1483 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1486 let raw = self.raw.as_ptr();
1487 collect_matches(|ptr, len| unsafe {
1488 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
1489 })
1490 }
1491
1492 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
1502 let mut change = ffi::TwigChange {
1503 old_span: ffi::TwigSpan { start: 0, end: 0 },
1504 new_span: ffi::TwigSpan { start: 0, end: 0 },
1505 };
1506 let status = unsafe {
1507 ffi::twig_editor_edit_range(
1508 self.raw.as_ptr(),
1509 start,
1510 end,
1511 text.as_ptr(),
1512 text.len(),
1513 &mut change,
1514 )
1515 };
1516 Error::from_status(status)?;
1517 Ok(Change::from_ffi(change))
1518 }
1519
1520 pub fn last_change(&mut self) -> Option<Change> {
1526 let mut change = ffi::TwigChange {
1527 old_span: ffi::TwigSpan { start: 0, end: 0 },
1528 new_span: ffi::TwigSpan { start: 0, end: 0 },
1529 };
1530 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
1531 match status.0 {
1532 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
1533 _ => None,
1534 }
1535 }
1536
1537 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
1542 let mut change = ffi::TwigChange {
1543 old_span: ffi::TwigSpan { start: 0, end: 0 },
1544 new_span: ffi::TwigSpan { start: 0, end: 0 },
1545 };
1546 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
1547 if status.0 == ffi::TwigStatus::NOT_FOUND {
1548 return Ok(None);
1549 }
1550 Error::from_status(status)?;
1551 Ok(Some(Change::from_ffi(change)))
1552 }
1553
1554 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
1558 let mut change = ffi::TwigChange {
1559 old_span: ffi::TwigSpan { start: 0, end: 0 },
1560 new_span: ffi::TwigSpan { start: 0, end: 0 },
1561 };
1562 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
1563 if status.0 == ffi::TwigStatus::NOT_FOUND {
1564 return Ok(None);
1565 }
1566 Error::from_status(status)?;
1567 Ok(Some(Change::from_ffi(change)))
1568 }
1569
1570 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
1575 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
1576 Error::from_status(status)
1577 }
1578
1579 pub fn revision(&mut self) -> u64 {
1585 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
1586 }
1587
1588 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
1609 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1610 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
1611 match status.0 {
1612 ffi::TwigStatus::OK => Some(span.start..span.end),
1613 _ => None,
1614 }
1615 }
1616
1617 pub fn clear_dirty(&mut self) {
1622 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
1623 }
1624
1625 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
1633 let status = unsafe {
1634 ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len())
1635 };
1636 Error::from_status(status)
1637 }
1638
1639 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
1644 let raw = self.raw.as_ptr();
1645 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
1646 }
1647
1648 pub fn document(&mut self) -> Result<DocumentView<'_>, Error> {
1657 let mut raw = std::ptr::null_mut();
1658 let status = unsafe { ffi::twig_editor_document(self.raw.as_ptr(), &mut raw) };
1659 Error::from_status(status)?;
1660 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1661 Ok(DocumentView {
1662 doc: Document { raw },
1663 _editor: PhantomData,
1664 })
1665 }
1666
1667 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1672 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1673 let mut len = 0usize;
1674 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
1675 Error::from_status(status)?;
1676 if len == 0 {
1677 return Ok(Vec::new());
1678 }
1679 if ptr.is_null() {
1680 return Err(Error::Internal);
1681 }
1682 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1683 raw.iter().map(flat_node_from_ffi).collect()
1684 }
1685
1686 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1693 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1694 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1695 let mut len = 0usize;
1696 let status =
1697 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
1698 Error::from_status(status)?;
1699 if len == 0 || ptr.is_null() {
1700 return Ok(Vec::new());
1701 }
1702 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1703 raw.iter().map(query_match_from_ffi).collect()
1704 }
1705
1706 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1714 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1715 let mut len = 0usize;
1716 let status =
1717 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
1718 Error::from_status(status)?;
1719 if len == 0 || ptr.is_null() {
1720 return Ok(Vec::new());
1721 }
1722 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1723 raw.iter().map(flat_node_from_ffi).collect()
1724 }
1725
1726 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1731 let mut m = ffi::TwigQueryMatch {
1732 node_id: 0,
1733 span: ffi::TwigSpan { start: 0, end: 0 },
1734 content_span: ffi::TwigSpan { start: 0, end: 0 },
1735 has_content_span: 0,
1736 kind: std::ptr::null(),
1737 };
1738 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
1739 match status.0 {
1740 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1741 ffi::TwigStatus::NOT_FOUND => Ok(None),
1742 _ => Err(Error::from_status(status).unwrap_err()),
1743 }
1744 }
1745
1746 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1750 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1751 let mut len = 0usize;
1752 let status =
1753 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
1754 match status.0 {
1755 ffi::TwigStatus::OK => {}
1756 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1757 _ => return Err(Error::from_status(status).unwrap_err()),
1758 }
1759 if len == 0 || ptr.is_null() {
1760 return Ok(Vec::new());
1761 }
1762 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1763 raw.iter().map(query_match_from_ffi).collect()
1764 }
1765
1766 pub fn wrap_range(
1774 &mut self,
1775 start: usize,
1776 end: usize,
1777 kind: InlineKind,
1778 ) -> Result<Change, Error> {
1779 self.change_op(|ed, out| unsafe {
1780 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
1781 })
1782 }
1783
1784 pub fn toggle_inline(
1789 &mut self,
1790 start: usize,
1791 end: usize,
1792 kind: InlineKind,
1793 ) -> Result<Change, Error> {
1794 self.change_op(|ed, out| unsafe {
1795 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
1796 })
1797 }
1798
1799 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
1818 let (block_kind, level) = kind.to_c();
1819 self.change_op(|ed, out| unsafe {
1820 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
1821 })
1822 }
1823
1824 pub fn toggle_block_container(
1847 &mut self,
1848 start: usize,
1849 end: usize,
1850 kind: BlockContainerKind,
1851 ) -> Result<Change, Error> {
1852 self.change_op(|ed, out| unsafe {
1853 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
1854 })
1855 }
1856
1857 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
1876 self.change_op(|ed, out| unsafe {
1877 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
1878 })?;
1879 Ok(())
1880 }
1881
1882 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
1891 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
1892 }
1893
1894 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
1897 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
1898 }
1899
1900 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
1902 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
1903 }
1904
1905 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
1907 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
1908 }
1909
1910 pub fn table_set_alignment(
1912 &mut self,
1913 offset: usize,
1914 alignment: Alignment,
1915 ) -> Result<(), Error> {
1916 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
1917 }
1918
1919 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
1921 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
1922 }
1923
1924 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
1926 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
1927 }
1928
1929 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
1930 self.change_op(|ed, out| unsafe { ffi::twig_editor_table_edit(ed, offset, op, arg, out) })?;
1931 Ok(())
1932 }
1933
1934 pub fn insert_link(
1979 &mut self,
1980 start: usize,
1981 end: usize,
1982 destination: &str,
1983 ) -> Result<Change, Error> {
1984 self.change_op(|ed, out| unsafe {
1985 ffi::twig_editor_insert_link(
1986 ed,
1987 start,
1988 end,
1989 destination.as_ptr(),
1990 destination.len(),
1991 out,
1992 )
1993 })
1994 }
1995
1996 pub fn insert_image(
2017 &mut self,
2018 start: usize,
2019 end: usize,
2020 destination: &str,
2021 ) -> Result<Change, Error> {
2022 self.change_op(|ed, out| unsafe {
2023 ffi::twig_editor_insert_image(
2024 ed,
2025 start,
2026 end,
2027 destination.as_ptr(),
2028 destination.len(),
2029 out,
2030 )
2031 })
2032 }
2033
2034 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
2055 self.change_op(|ed, out| unsafe {
2056 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
2057 })
2058 }
2059
2060 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
2074 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
2075 }
2076
2077 pub fn insert_thematic_break(&mut self, offset: usize) -> Result<Change, Error> {
2096 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_thematic_break(ed, offset, out) })
2097 }
2098
2099 pub fn split_block(&mut self, offset: usize) -> Result<Change, Error> {
2147 self.change_op(|ed, out| unsafe { ffi::twig_editor_split_block(ed, offset, out) })
2148 }
2149
2150 pub fn toggle_code_block(
2182 &mut self,
2183 start: usize,
2184 end: usize,
2185 language: Option<&str>,
2186 ) -> Result<Change, Error> {
2187 let (ptr, len, has) = opt_str(language);
2188 self.change_op(|ed, out| unsafe {
2189 ffi::twig_editor_toggle_code_block(ed, start, end, ptr, len, has, out)
2190 })
2191 }
2192
2193 pub fn set_code_language(
2203 &mut self,
2204 offset: usize,
2205 language: Option<&str>,
2206 ) -> Result<Change, Error> {
2207 let (ptr, len, has) = opt_str(language);
2208 self.change_op(|ed, out| unsafe {
2209 ffi::twig_editor_set_code_language(ed, offset, ptr, len, has, out)
2210 })
2211 }
2212
2213 pub fn toggle_task_item(&mut self, offset: usize) -> Result<Change, Error> {
2224 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_item(ed, offset, out) })
2225 }
2226
2227 pub fn set_task_checked(&mut self, offset: usize, checked: bool) -> Result<(), Error> {
2241 self.change_op(|ed, out| unsafe {
2242 ffi::twig_editor_set_task_checked(ed, offset, checked as c_int, out)
2243 })?;
2244 Ok(())
2245 }
2246
2247 pub fn toggle_task_checked(&mut self, offset: usize) -> Result<Change, Error> {
2252 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_checked(ed, offset, out) })
2253 }
2254
2255 pub fn insert_footnote(&mut self, offset: usize, label: &str) -> Result<Change, Error> {
2274 self.change_op(|ed, out| unsafe {
2275 ffi::twig_editor_insert_footnote(ed, offset, label.as_ptr(), label.len(), out)
2276 })
2277 }
2278
2279 fn change_op(
2282 &mut self,
2283 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
2284 ) -> Result<Change, Error> {
2285 let mut change = ffi::TwigChange {
2286 old_span: ffi::TwigSpan { start: 0, end: 0 },
2287 new_span: ffi::TwigSpan { start: 0, end: 0 },
2288 };
2289 let status = op(self.raw.as_ptr(), &mut change);
2290 Error::from_status(status)?;
2291 Ok(Change::from_ffi(change))
2292 }
2293
2294 fn apply(
2296 &mut self,
2297 locator: &str,
2298 text: &str,
2299 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
2300 ) -> Result<(), Error> {
2301 let status = op(
2302 self.raw.as_ptr(),
2303 locator.as_ptr(),
2304 locator.len(),
2305 text.as_ptr(),
2306 text.len(),
2307 );
2308 Error::from_status(status)
2309 }
2310}
2311
2312impl Drop for Editor {
2313 fn drop(&mut self) {
2314 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
2315 }
2316}
2317
2318fn collect_bytes(
2323 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
2324) -> Result<Vec<u8>, Error> {
2325 let mut ptr = std::ptr::null();
2326 let mut len = 0usize;
2327 let status = call(&mut ptr, &mut len);
2328 Error::from_status(status)?;
2329 if len == 0 {
2330 return Ok(Vec::new());
2331 }
2332 if ptr.is_null() {
2333 return Err(Error::Internal);
2334 }
2335 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2336 Ok(bytes.to_vec())
2337}
2338
2339fn collect_matches(
2342 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
2343) -> Result<Vec<QueryMatch>, Error> {
2344 let mut ptr = std::ptr::null();
2345 let mut len = 0usize;
2346 let status = call(&mut ptr, &mut len);
2347 Error::from_status(status)?;
2348 if len == 0 {
2349 return Ok(Vec::new());
2350 }
2351 if ptr.is_null() {
2352 return Err(Error::Internal);
2353 }
2354 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
2355 matches.iter().map(query_match_from_ffi).collect()
2356}
2357
2358fn collect_flat_nodes(
2361 call: impl FnOnce(*mut *const ffi::TwigFlatNode, *mut usize) -> ffi::TwigStatus,
2362) -> Result<Vec<FlatNode>, Error> {
2363 let mut ptr = std::ptr::null();
2364 let mut len = 0usize;
2365 let status = call(&mut ptr, &mut len);
2366 Error::from_status(status)?;
2367 if len == 0 {
2368 return Ok(Vec::new());
2369 }
2370 if ptr.is_null() {
2371 return Err(Error::Internal);
2372 }
2373 let nodes = unsafe { std::slice::from_raw_parts(ptr, len) };
2374 nodes.iter().map(flat_node_from_ffi).collect()
2375}
2376
2377fn empty_ffi_match() -> ffi::TwigQueryMatch {
2379 ffi::TwigQueryMatch {
2380 node_id: 0,
2381 span: ffi::TwigSpan { start: 0, end: 0 },
2382 content_span: ffi::TwigSpan { start: 0, end: 0 },
2383 has_content_span: 0,
2384 kind: std::ptr::null(),
2385 }
2386}
2387
2388fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
2391 Ok(QueryMatch {
2392 node_id: m.node_id,
2393 span: m.span.start..m.span.end,
2394 content_span: if m.has_content_span != 0 {
2395 Some(m.content_span.start..m.content_span.end)
2396 } else {
2397 None
2398 },
2399 kind: Kind::from(borrowed_cstr(m.kind)?.as_str()),
2400 })
2401}
2402
2403fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
2405 let node_id = |v: u32| {
2406 if v == ffi::TWIG_NO_NODE {
2407 None
2408 } else {
2409 Some(NodeId(v))
2410 }
2411 };
2412 Ok(FlatNode {
2413 id: NodeId(n.id),
2414 parent: node_id(n.parent),
2415 first_child: node_id(n.first_child),
2416 next_sibling: node_id(n.next_sibling),
2417 span: n.span.start..n.span.end,
2418 content_span: if n.has_content_span != 0 {
2419 Some(n.content_span.start..n.content_span.end)
2420 } else {
2421 None
2422 },
2423 level: if n.level != 0 { Some(n.level) } else { None },
2424 kind: Kind::from(borrowed_cstr(n.kind)?.as_str()),
2425 text: borrowed_bytes(n.text_ptr, n.text_len),
2426 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
2427 head: match n.head {
2428 ffi::TWIG_HEAD_NONE => None,
2429 v => Some(v != 0),
2430 },
2431 alignment: Alignment::from_c(n.alignment),
2432 name: borrowed_bytes(n.name_ptr, n.name_len),
2433 directive_form: DirectiveForm::from_c(n.directive_form),
2434 origin: ContainerOrigin::from_c(n.container_origin),
2435 marker_span: if n.has_marker_span != 0 {
2436 Some(n.marker_span.start..n.marker_span.end)
2437 } else {
2438 None
2439 },
2440 checked: match n.checked {
2441 ffi::TWIG_TASK_CHECKED_NONE => None,
2442 v => Some(v != 0),
2443 },
2444 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
2445 })
2446}
2447
2448fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
2452 if ptr.is_null() || len == 0 {
2453 return Vec::new();
2454 }
2455 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
2456 kvs.iter()
2457 .map(|kv| {
2458 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
2459 (key, borrowed_bytes(kv.value, kv.value_len))
2460 })
2461 .collect()
2462}
2463
2464fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
2466 if ptr.is_null() {
2467 return Err(Error::Internal);
2468 }
2469 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
2470 .to_str()
2471 .map_err(|_| Error::Internal)?
2472 .to_owned())
2473}
2474
2475fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
2479 if ptr.is_null() {
2480 return None;
2481 }
2482 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2483 Some(String::from_utf8_lossy(bytes).into_owned())
2484}
2485
2486#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
2490pub struct NodeId(pub u32);
2491
2492#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2495pub enum VoidKind {
2496 Doc,
2497 Para,
2498 ThematicBreak,
2499 Section,
2500 Div,
2501 BlockQuote,
2502 DefinitionList,
2503 Table,
2504 ListItem,
2505 DefinitionListItem,
2506 Term,
2507 Definition,
2508 Caption,
2509 SoftBreak,
2510 HardBreak,
2511 NonBreakingSpace,
2512 Emph,
2513 Strong,
2514 Span,
2515 Mark,
2516 Superscript,
2517 Subscript,
2518 Insert,
2519 Delete,
2520 DoubleQuoted,
2521 SingleQuoted,
2522}
2523
2524impl VoidKind {
2525 fn to_c(self) -> c_int {
2526 match self {
2528 VoidKind::Doc => 0,
2529 VoidKind::Para => 1,
2530 VoidKind::ThematicBreak => 3,
2531 VoidKind::Section => 4,
2532 VoidKind::Div => 5,
2533 VoidKind::BlockQuote => 9,
2534 VoidKind::DefinitionList => 13,
2535 VoidKind::Table => 14,
2536 VoidKind::ListItem => 15,
2537 VoidKind::DefinitionListItem => 17,
2538 VoidKind::Term => 18,
2539 VoidKind::Definition => 19,
2540 VoidKind::Caption => 22,
2541 VoidKind::SoftBreak => 26,
2542 VoidKind::HardBreak => 27,
2543 VoidKind::NonBreakingSpace => 28,
2544 VoidKind::Emph => 38,
2545 VoidKind::Strong => 39,
2546 VoidKind::Span => 42,
2547 VoidKind::Mark => 43,
2548 VoidKind::Superscript => 44,
2549 VoidKind::Subscript => 45,
2550 VoidKind::Insert => 46,
2551 VoidKind::Delete => 47,
2552 VoidKind::DoubleQuoted => 48,
2553 VoidKind::SingleQuoted => 49,
2554 }
2555 }
2556}
2557
2558#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2560pub enum TextKind {
2561 Str,
2562 Symb,
2563 Verbatim,
2564 InlineMath,
2565 DisplayMath,
2566 Url,
2567 Email,
2568 FootnoteReference,
2569 CitationReference,
2572 SubstitutionReference,
2574 Comment,
2575 Doctype,
2576 Cdata,
2577}
2578
2579impl TextKind {
2580 fn to_c(self) -> c_int {
2581 match self {
2582 TextKind::Str => 25,
2583 TextKind::Symb => 29,
2584 TextKind::Verbatim => 30,
2585 TextKind::InlineMath => 32,
2586 TextKind::DisplayMath => 33,
2587 TextKind::Url => 34,
2588 TextKind::Email => 35,
2589 TextKind::FootnoteReference => 36,
2590 TextKind::CitationReference => 58,
2591 TextKind::SubstitutionReference => 59,
2592 TextKind::Comment => 52,
2593 TextKind::Doctype => 53,
2594 TextKind::Cdata => 55,
2595 }
2596 }
2597}
2598
2599#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2601pub enum BulletStyle {
2602 Dash,
2603 Plus,
2604 Star,
2605}
2606
2607impl BulletStyle {
2608 fn to_c(self) -> c_int {
2609 match self {
2610 BulletStyle::Dash => 0,
2611 BulletStyle::Plus => 1,
2612 BulletStyle::Star => 2,
2613 }
2614 }
2615}
2616
2617#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2619pub enum OrderedNumbering {
2620 Decimal,
2621 LowerAlpha,
2622 UpperAlpha,
2623 LowerRoman,
2624 UpperRoman,
2625}
2626
2627impl OrderedNumbering {
2628 fn to_c(self) -> c_int {
2629 match self {
2630 OrderedNumbering::Decimal => 0,
2631 OrderedNumbering::LowerAlpha => 1,
2632 OrderedNumbering::UpperAlpha => 2,
2633 OrderedNumbering::LowerRoman => 3,
2634 OrderedNumbering::UpperRoman => 4,
2635 }
2636 }
2637}
2638
2639#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2641pub enum OrderedDelim {
2642 Period,
2643 ParenAfter,
2644 ParenBoth,
2645}
2646
2647impl OrderedDelim {
2648 fn to_c(self) -> c_int {
2649 match self {
2650 OrderedDelim::Period => 0,
2651 OrderedDelim::ParenAfter => 1,
2652 OrderedDelim::ParenBoth => 2,
2653 }
2654 }
2655}
2656
2657#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2660pub enum Alignment {
2661 Default,
2662 Left,
2663 Right,
2664 Center,
2665}
2666
2667impl Alignment {
2668 fn to_c(self) -> c_int {
2669 match self {
2670 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
2671 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
2672 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
2673 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
2674 }
2675 }
2676
2677 fn from_c(v: c_int) -> Option<Self> {
2680 match v {
2681 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
2682 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
2683 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
2684 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
2685 _ => None,
2686 }
2687 }
2688}
2689
2690#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2692pub enum SmartPunctuation {
2693 LeftSingleQuote,
2694 RightSingleQuote,
2695 LeftDoubleQuote,
2696 RightDoubleQuote,
2697 Ellipses,
2698 EmDash,
2699 EnDash,
2700}
2701
2702impl SmartPunctuation {
2703 fn to_c(self) -> c_int {
2704 match self {
2705 SmartPunctuation::LeftSingleQuote => 0,
2706 SmartPunctuation::RightSingleQuote => 1,
2707 SmartPunctuation::LeftDoubleQuote => 2,
2708 SmartPunctuation::RightDoubleQuote => 3,
2709 SmartPunctuation::Ellipses => 4,
2710 SmartPunctuation::EmDash => 5,
2711 SmartPunctuation::EnDash => 6,
2712 }
2713 }
2714}
2715
2716#[derive(Clone, Debug, Eq, PartialEq)]
2731pub struct Warning {
2732 pub fidelity: Fidelity,
2733 pub path: String,
2740 pub kind: Kind,
2743}
2744
2745#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2747#[non_exhaustive]
2748pub enum Fidelity {
2749 Degraded,
2752 Dropped,
2754}
2755
2756impl Fidelity {
2757 fn from_c(v: c_int) -> Self {
2761 match v {
2762 ffi::TWIG_FIDELITY_DROPPED => Fidelity::Dropped,
2763 _ => Fidelity::Degraded,
2764 }
2765 }
2766}
2767
2768#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2769#[non_exhaustive]
2770pub enum ContainerOrigin {
2771 Element,
2773 Directive,
2777}
2778
2779impl ContainerOrigin {
2780 fn from_c(v: c_int) -> Option<Self> {
2783 match v {
2784 ffi::TWIG_CONTAINER_ORIGIN_ELEMENT => Some(ContainerOrigin::Element),
2785 ffi::TWIG_CONTAINER_ORIGIN_DIRECTIVE => Some(ContainerOrigin::Directive),
2786 _ => None,
2787 }
2788 }
2789}
2790
2791#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2793pub enum DirectiveForm {
2794 Text,
2795 Leaf,
2796 Container,
2797}
2798
2799impl DirectiveForm {
2800 fn to_c(self) -> c_int {
2801 match self {
2802 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
2803 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
2804 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
2805 }
2806 }
2807
2808 fn from_c(v: c_int) -> Option<Self> {
2812 match v {
2813 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
2814 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
2815 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
2816 _ => None,
2817 }
2818 }
2819}
2820
2821fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
2825 match s {
2826 Some(x) => (x.as_ptr(), x.len(), 1),
2827 None => (std::ptr::null(), 0, 0),
2828 }
2829}
2830
2831#[derive(Debug)]
2838pub struct Builder {
2839 raw: NonNull<ffi::TwigBuilder>,
2840}
2841
2842impl Builder {
2843 pub fn new() -> Result<Self, Error> {
2845 let mut raw = std::ptr::null_mut();
2846 let status = unsafe { ffi::twig_builder_create(&mut raw) };
2847 Error::from_status(status)?;
2848 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
2849 Ok(Self { raw })
2850 }
2851
2852 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
2855 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
2856 }
2857
2858 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
2860 self.emit(|b, out| unsafe {
2861 ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out)
2862 })
2863 }
2864
2865 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
2867 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
2868 }
2869
2870 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
2872 let (lp, ll, has) = opt_str(lang);
2873 self.emit(|b, out| unsafe {
2874 ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out)
2875 })
2876 }
2877
2878 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
2880 self.emit(|b, out| unsafe {
2881 ffi::twig_builder_add_raw_block(
2882 b,
2883 format.as_ptr(),
2884 format.len(),
2885 text.as_ptr(),
2886 text.len(),
2887 out,
2888 )
2889 })
2890 }
2891
2892 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
2894 self.emit(|b, out| unsafe {
2895 ffi::twig_builder_add_metadata(
2896 b,
2897 lang.as_ptr(),
2898 lang.len(),
2899 text.as_ptr(),
2900 text.len(),
2901 out,
2902 )
2903 })
2904 }
2905
2906 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
2908 self.emit(|b, out| unsafe {
2909 ffi::twig_builder_add_raw_inline(
2910 b,
2911 format.as_ptr(),
2912 format.len(),
2913 text.as_ptr(),
2914 text.len(),
2915 out,
2916 )
2917 })
2918 }
2919
2920 pub fn add_smart_punctuation(
2925 &mut self,
2926 kind: SmartPunctuation,
2927 text: &str,
2928 ) -> Result<NodeId, Error> {
2929 self.emit(|b, out| unsafe {
2930 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
2931 })
2932 }
2933
2934 pub fn add_link(
2937 &mut self,
2938 destination: Option<&str>,
2939 reference: Option<&str>,
2940 ) -> Result<NodeId, Error> {
2941 let (dp, dl, hd) = opt_str(destination);
2942 let (rp, rl, hr) = opt_str(reference);
2943 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
2944 }
2945
2946 pub fn add_image(
2948 &mut self,
2949 destination: Option<&str>,
2950 reference: Option<&str>,
2951 ) -> Result<NodeId, Error> {
2952 let (dp, dl, hd) = opt_str(destination);
2953 let (rp, rl, hr) = opt_str(reference);
2954 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
2955 }
2956
2957 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
2959 self.emit(|b, out| unsafe {
2960 ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out)
2961 })
2962 }
2963
2964 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
2966 self.emit(|b, out| unsafe {
2967 ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out)
2968 })
2969 }
2970
2971 pub fn add_processing_instruction(
2973 &mut self,
2974 target: &str,
2975 data: &str,
2976 ) -> Result<NodeId, Error> {
2977 self.emit(|b, out| unsafe {
2978 ffi::twig_builder_add_processing_instruction(
2979 b,
2980 target.as_ptr(),
2981 target.len(),
2982 data.as_ptr(),
2983 data.len(),
2984 out,
2985 )
2986 })
2987 }
2988
2989 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
2991 self.emit(|b, out| unsafe {
2992 ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out)
2993 })
2994 }
2995
2996 pub fn add_citation(&mut self, label: &str) -> Result<NodeId, Error> {
3001 self.emit(|b, out| unsafe {
3002 ffi::twig_builder_add_citation(b, label.as_ptr(), label.len(), out)
3003 })
3004 }
3005
3006 pub fn add_substitution(&mut self, label: &str) -> Result<NodeId, Error> {
3010 self.emit(|b, out| unsafe {
3011 ffi::twig_builder_add_substitution(b, label.as_ptr(), label.len(), out)
3012 })
3013 }
3014
3015 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
3017 self.emit(|b, out| unsafe {
3018 ffi::twig_builder_add_reference(
3019 b,
3020 label.as_ptr(),
3021 label.len(),
3022 destination.as_ptr(),
3023 destination.len(),
3024 out,
3025 )
3026 })
3027 }
3028
3029 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
3031 self.emit(|b, out| unsafe {
3032 ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out)
3033 })
3034 }
3035
3036 pub fn add_ordered_list(
3038 &mut self,
3039 numbering: OrderedNumbering,
3040 delim: OrderedDelim,
3041 tight: bool,
3042 start: Option<u32>,
3043 ) -> Result<NodeId, Error> {
3044 let (start_val, has_start) = match start {
3045 Some(s) => (s, 1),
3046 None => (0, 0),
3047 };
3048 self.emit(|b, out| unsafe {
3049 ffi::twig_builder_add_ordered_list(
3050 b,
3051 numbering.to_c(),
3052 delim.to_c(),
3053 tight as c_int,
3054 start_val,
3055 has_start,
3056 out,
3057 )
3058 })
3059 }
3060
3061 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
3063 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
3064 }
3065
3066 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
3068 self.emit(|b, out| unsafe {
3069 ffi::twig_builder_add_task_list_item(b, checked as c_int, out)
3070 })
3071 }
3072
3073 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
3075 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
3076 }
3077
3078 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
3080 self.emit(|b, out| unsafe {
3081 ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out)
3082 })
3083 }
3084
3085 pub fn add_cell_spanning(
3090 &mut self,
3091 head: bool,
3092 alignment: Alignment,
3093 colspan: u32,
3094 rowspan: u32,
3095 ) -> Result<NodeId, Error> {
3096 self.emit(|b, out| unsafe {
3097 ffi::twig_builder_add_cell_spanning(
3098 b,
3099 head as c_int,
3100 alignment.to_c(),
3101 colspan,
3102 rowspan,
3103 out,
3104 )
3105 })
3106 }
3107
3108 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
3111 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
3112 let status = unsafe {
3113 ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len())
3114 };
3115 Error::from_status(status)
3116 }
3117
3118 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
3122 let kvs: Vec<ffi::TwigKeyVal> = attrs
3123 .iter()
3124 .map(|(k, v)| ffi::TwigKeyVal {
3125 key: k.as_ptr(),
3126 key_len: k.len(),
3127 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
3128 value_len: v.map_or(0, |s| s.len()),
3129 })
3130 .collect();
3131 let status = unsafe {
3132 ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len())
3133 };
3134 Error::from_status(status)
3135 }
3136
3137 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3140 let raw = self.raw.as_ptr();
3141 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
3142 }
3143
3144 pub fn serialize_to(&mut self, root: NodeId, target: Target) -> Result<Vec<u8>, Error> {
3151 let raw = self.raw.as_ptr();
3152 let ffi_target: ffi::TwigFormat = target.into();
3153 collect_bytes(|ptr, len| unsafe {
3154 ffi::twig_builder_serialize(raw, root.0, ffi_target as i32, ptr, len)
3155 })
3156 }
3157
3158 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
3163 self.serialize_to(root, format.into())
3164 }
3165
3166 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3168 let raw = self.raw.as_ptr();
3169 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
3170 }
3171
3172 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
3175 let raw = self.raw.as_ptr();
3176 collect_matches(|ptr, len| unsafe {
3177 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
3178 })
3179 }
3180
3181 fn emit(
3184 &mut self,
3185 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
3186 ) -> Result<NodeId, Error> {
3187 let mut id: u32 = 0;
3188 let status = call(self.raw.as_ptr(), &mut id);
3189 Error::from_status(status)?;
3190 Ok(NodeId(id))
3191 }
3192}
3193
3194impl Drop for Builder {
3195 fn drop(&mut self) {
3196 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
3197 }
3198}
3199
3200#[cfg(test)]
3201mod tests {
3202 use super::*;
3203
3204 #[test]
3205 fn abi_version_matches() {
3206 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
3210 }
3211
3212 #[test]
3213 fn parses_and_renders_markdown_html() {
3214 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3215 let html = doc.render_html().expect("render html");
3216 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
3217 }
3218
3219 #[test]
3220 fn parses_html_input() {
3221 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
3222 let html = doc.render_html().expect("render html");
3223 assert!(String::from_utf8_lossy(&html).contains("hi"));
3224 }
3225
3226 #[test]
3227 fn parses_asciidoc_and_refuses_to_write_it() {
3228 let mut doc = Document::parse_str("= Title\n\nsome *bold* text\n", Format::Asciidoc)
3229 .expect("parse asciidoc");
3230 let html = String::from_utf8_lossy(&doc.render_html().expect("render html")).into_owned();
3231 assert!(html.contains("<h1>Title</h1>"), "got {html:?}");
3232 assert!(html.contains("<strong>bold</strong>"), "got {html:?}");
3233
3234 assert_eq!(
3238 doc.serialize_to(Target::Asciidoc),
3239 Err(Error::UnsupportedFormat)
3240 );
3241 assert_eq!(Target::from(Format::Asciidoc), Target::Asciidoc);
3242 assert_eq!(Target::Asciidoc.as_format(), Some(Format::Asciidoc));
3243 }
3244
3245 #[test]
3246 fn serialize_round_trips_and_cross_converts() {
3247 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3248
3249 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
3250 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
3251
3252 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
3254 }
3255
3256 #[test]
3257 fn serialize_markdown_to_djot() {
3258 let mut doc =
3259 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3260 let djot = doc.serialize(Format::Djot).expect("serialize djot");
3261 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3262 }
3263
3264 #[test]
3265 fn serialize_to_takes_the_output_axis() {
3266 let mut doc =
3267 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3268
3269 let djot = doc.serialize_to(Target::Djot).expect("serialize djot");
3270 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3271
3272 assert_eq!(doc.serialize_to(Target::Xml), Err(Error::UnsupportedFormat));
3275 }
3276
3277 #[test]
3278 fn serialize_and_serialize_to_agree() {
3279 let mut a = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3282 let mut b = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3283 for format in [Format::Markdown, Format::Djot, Format::Html] {
3284 assert_eq!(a.serialize(format), b.serialize_to(Target::from(format)));
3285 }
3286 }
3287
3288 #[test]
3289 fn every_format_is_a_target_that_names_it_back() {
3290 for format in [Format::Djot, Format::Markdown, Format::Xml, Format::Html] {
3293 assert_eq!(Target::from(format).as_format(), Some(format));
3294 }
3295 }
3296
3297 #[test]
3298 fn ast_json_dumps_the_tree() {
3299 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
3300 let json = doc.ast_json().expect("ast json");
3301 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
3302 }
3303
3304 #[test]
3305 fn query_finds_nodes_by_selector() {
3306 let source = "# One\n\n## Two\n";
3307 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3308 let matches = doc.query("heading").expect("query");
3309
3310 assert_eq!(matches.len(), 2);
3311 for m in &matches {
3312 assert_eq!(m.kind, Kind::Heading);
3313 assert!(m.span.start < m.span.end);
3314 }
3315 }
3316
3317 #[test]
3318 fn query_recovers_code_spans() {
3319 let source = "prose `code` more prose\n";
3320 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3321 let matches = doc.query("verbatim").expect("query");
3322
3323 assert_eq!(matches.len(), 1);
3324 assert_eq!(&source[matches[0].span.clone()], "`code`");
3325 }
3326
3327 #[test]
3328 fn document_span_accessors_read_by_node_id() {
3329 let source = "# hi\n\ntext\n";
3330 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3331 let heading = doc.query("heading").expect("query").pop().expect("heading");
3332
3333 assert_eq!(
3334 doc.span(NodeId(heading.node_id)).expect("span"),
3335 heading.span
3336 );
3337 assert_eq!(
3338 doc.content_span(NodeId(heading.node_id))
3339 .expect("content span"),
3340 heading.content_span
3341 );
3342 assert_eq!(doc.span(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3343 }
3344
3345 #[test]
3346 fn document_walks_its_tree_without_an_editor() {
3347 let source = "# hi\n\ntext\n";
3348 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3349
3350 let nodes = doc.nodes().expect("nodes");
3351 assert!(nodes.len() >= 3);
3352 for (i, n) in nodes.iter().enumerate() {
3353 assert_eq!(n.id, NodeId(i as u32));
3354 }
3355
3356 let kids = doc.children(None).expect("children");
3357 assert_eq!(kids.len(), 2);
3358 assert_eq!(kids[0].kind, Kind::Heading);
3359
3360 let sub = doc.subtree(NodeId(kids[0].node_id)).expect("subtree");
3361 assert_eq!(sub[0].id, NodeId(0));
3362 assert_eq!(sub[0].parent, None);
3363 assert_eq!(sub[0].span, kids[0].span);
3364
3365 let hit = doc.node_at(2).expect("node_at").expect("a node at 2");
3366 let chain = doc.ancestors_at(2).expect("ancestors");
3367 assert_eq!(chain.last().expect("deepest").node_id, hit.node_id);
3368 assert_eq!(chain[0].kind, Kind::Doc);
3369
3370 assert_eq!(doc.subtree(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3371 }
3372
3373 #[test]
3374 fn editor_document_view_reads_the_live_tree() {
3375 let mut ed = Editor::new_str("# one\n\ntwo\n", Format::Markdown).expect("editor");
3376
3377 {
3378 let mut view = ed.document().expect("view");
3379 let kids = view.children(None).expect("children");
3380 assert_eq!(kids.len(), 2);
3381 assert_eq!(kids[0].kind, Kind::Heading);
3382 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..5);
3383 assert_eq!(view.render_html(), Err(Error::UnsupportedFormat));
3385 assert_eq!(
3386 view.serialize(Format::Markdown),
3387 Err(Error::UnsupportedFormat)
3388 );
3389 }
3390
3391 ed.replace("0", "# one and a half").expect("replace");
3392 let mut view = ed.document().expect("view");
3393 let kids = view.children(None).expect("children");
3394 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..16);
3395 }
3396
3397 #[test]
3398 fn query_rejects_a_malformed_selector() {
3399 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
3400 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
3401 }
3402
3403 #[test]
3404 fn editor_edits_by_index_path() {
3405 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
3406 ed.replace_content("0.0", "bye").expect("replace_content");
3407 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
3408 }
3409
3410 #[test]
3411 fn flat_nodes_expose_element_name_and_attrs() {
3412 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
3416 let mut ed = Editor::new_ext(
3417 src.as_bytes(),
3418 Format::Markdown,
3419 MarkdownExtensions {
3420 html_elements: true,
3421 ..Default::default()
3422 },
3423 )
3424 .expect("editor");
3425 let nodes = ed.nodes().expect("nodes");
3426
3427 let source = nodes
3428 .iter()
3429 .find(|n| n.name.as_deref() == Some("source"))
3430 .expect("a <source> element node");
3431 assert_eq!(
3432 source.attrs,
3433 vec![
3434 (
3435 "media".to_string(),
3436 Some("(prefers-color-scheme: dark)".to_string())
3437 ),
3438 ("srcset".to_string(), Some("d.svg".to_string())),
3439 ]
3440 );
3441
3442 let img = nodes
3445 .iter()
3446 .find(|n| n.kind == Kind::Image)
3447 .expect("an image node");
3448 assert!(img.name.is_none());
3449 assert_eq!(img.destination.as_deref(), Some("l.svg"));
3450
3451 let picture_kids_str = nodes.iter().find(|n| n.kind == Kind::Str);
3453 if let Some(s) = picture_kids_str {
3454 assert!(s.name.is_none() && s.attrs.is_empty());
3455 }
3456 }
3457
3458 #[test]
3459 fn definitions_finds_what_a_walk_from_the_root_cannot() {
3460 let mut doc = Document::parse_str(
3464 "text[^1] [x][a]\n\n[^1]: note\n\n[a]: /u\n",
3465 Format::Markdown,
3466 )
3467 .expect("parse markdown");
3468
3469 let defs = doc.definitions().expect("definitions");
3470 let mut kinds: Vec<Kind> = defs.iter().map(|m| m.kind.clone()).collect();
3471 kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3472 assert_eq!(kinds, vec![Kind::Footnote, Kind::Reference]);
3473
3474 let all = doc.nodes().expect("nodes");
3477 let root = all
3478 .iter()
3479 .find(|n| n.kind == Kind::Doc)
3480 .expect("a doc root");
3481 let mut reachable = vec![root.id];
3482 let mut i = 0;
3483 while i < reachable.len() {
3484 let n = &all[reachable[i].0 as usize];
3485 let mut c = n.first_child;
3486 while let Some(cid) = c {
3487 reachable.push(cid);
3488 c = all[cid.0 as usize].next_sibling;
3489 }
3490 i += 1;
3491 }
3492 for d in &defs {
3493 assert!(
3494 !reachable.contains(&NodeId(d.node_id)),
3495 "{} should be unreachable from the root",
3496 d.kind
3497 );
3498 }
3499
3500 let mut plain = Document::parse_str("just text\n", Format::Markdown).expect("parse");
3502 assert_eq!(plain.definitions().expect("definitions"), Vec::new());
3503 }
3504
3505 #[test]
3506 fn kind_round_trips_through_its_published_name() {
3507 for k in [
3511 Kind::Doc,
3512 Kind::Para,
3513 Kind::Heading,
3514 Kind::Container,
3515 Kind::TaskListItem,
3516 Kind::Superscript,
3517 Kind::FootnoteReference,
3518 Kind::ProcessingInstruction,
3519 Kind::Cdata,
3520 ] {
3521 assert_eq!(Kind::from(k.as_str()), k, "{k} did not round-trip");
3522 assert!(!k.is_unknown());
3523 }
3524 }
3525
3526 #[test]
3527 fn an_unknown_kind_name_is_carried_rather_than_lost() {
3528 let k = Kind::from("some_future_kind");
3531 assert!(k.is_unknown());
3532 assert_eq!(k.as_str(), "some_future_kind");
3533 assert_eq!(k, Kind::Other("some_future_kind".to_string()));
3534 }
3535
3536 #[test]
3537 fn every_kind_the_library_publishes_has_a_variant() {
3538 let cases: &[(&str, Format, MarkdownExtensions)] = &[
3543 (
3544 "# 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",
3545 Format::Markdown,
3546 MarkdownExtensions {
3547 directives: false,
3548 math: false,
3549 html_elements: false,
3550 },
3551 ),
3552 (
3553 "| 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",
3554 Format::Markdown,
3555 MarkdownExtensions::default(),
3556 ),
3557 (
3558 ":::note\nbody\n:::\n\n:role[x]\n\n$a+b$\n",
3559 Format::Markdown,
3560 MarkdownExtensions {
3561 directives: true,
3562 math: true,
3563 html_elements: false,
3564 },
3565 ),
3566 (
3567 "a^b^ c~d~ {=e=} {+f+} {-g-} 'q' \"dq\"\n\n\n\n<https://e.com>\n",
3568 Format::Djot,
3569 MarkdownExtensions::default(),
3570 ),
3571 (
3572 "<!-- c --><!DOCTYPE html><video controls><p>x</p></video>",
3573 Format::Html,
3574 MarkdownExtensions::default(),
3575 ),
3576 ];
3577
3578 let mut unknown: Vec<String> = Vec::new();
3579 let mut seen: Vec<String> = Vec::new();
3580 for (src, format, ext) in cases {
3581 let mut ed = Editor::new_ext(src.as_bytes(), *format, *ext).expect("editor");
3582 for n in ed.nodes().expect("nodes") {
3583 if n.kind.is_unknown() {
3584 unknown.push(n.kind.as_str().to_string());
3585 }
3586 seen.push(n.kind.as_str().to_string());
3587 }
3588 }
3589 unknown.sort();
3590 unknown.dedup();
3591 assert!(unknown.is_empty(), "kinds with no variant: {unknown:?}");
3592
3593 seen.sort();
3596 seen.dedup();
3597 assert!(
3598 seen.len() >= 30,
3599 "only {} distinct kinds reached: {seen:?}",
3600 seen.len()
3601 );
3602 }
3603
3604 #[test]
3605 fn diagnostics_report_what_a_conversion_would_lose() {
3606 let mut doc = Document::parse_str("a^b^ c\n", Format::Djot).expect("parse djot");
3610
3611 let to_md = doc
3612 .diagnostics(Target::Markdown)
3613 .expect("markdown diagnostics");
3614 assert_eq!(
3615 to_md,
3616 vec![Warning {
3617 fidelity: Fidelity::Degraded,
3618 path: "0/1".to_string(),
3619 kind: Kind::Superscript,
3620 }]
3621 );
3622
3623 assert_eq!(
3625 doc.diagnostics(Target::Djot).expect("djot diagnostics"),
3626 Vec::new()
3627 );
3628 }
3629
3630 #[test]
3631 fn diagnostics_separate_a_droppable_node_from_a_degradable_one() {
3632 let mut doc =
3636 Document::parse_str("<p>hi</p><!-- secret -->", Format::Html).expect("parse html");
3637 let warnings = doc.diagnostics(Target::Djot).expect("djot diagnostics");
3638 let comment = warnings
3639 .iter()
3640 .find(|w| w.kind == Kind::Comment)
3641 .expect("a warning about the comment");
3642 assert_eq!(comment.fidelity, Fidelity::Dropped);
3643 }
3644
3645 #[test]
3646 fn diagnostics_refuse_a_target_with_no_serializer() {
3647 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3650 assert_eq!(doc.diagnostics(Target::Xml), Err(Error::UnsupportedFormat));
3651 assert_eq!(
3652 doc.diagnostics(Target::Asciidoc),
3653 Err(Error::UnsupportedFormat)
3654 );
3655 }
3656
3657 #[test]
3658 fn diagnostics_flag_a_header_less_table_and_leave_a_headed_one_alone() {
3659 let mut headed = Document::parse_str(
3664 "<table><tr><th>H</th></tr><tr><td>a</td></tr></table>",
3665 Format::Html,
3666 )
3667 .expect("parse headed table");
3668 assert!(
3669 headed
3670 .diagnostics(Target::Markdown)
3671 .expect("diagnostics")
3672 .iter()
3673 .all(|w| w.kind != Kind::Table)
3674 );
3675
3676 let mut headless = Document::parse_str("<table><tr><td>a</td></tr></table>", Format::Html)
3677 .expect("parse header-less table");
3678 let table_warning = headless
3679 .diagnostics(Target::Markdown)
3680 .expect("diagnostics")
3681 .into_iter()
3682 .find(|w| w.kind == Kind::Table)
3683 .expect("a warning about the table");
3684 assert_eq!(table_warning.fidelity, Fidelity::Degraded);
3685 }
3686
3687 #[test]
3688 fn container_origin_separates_a_div_from_a_div() {
3689 let mut html =
3694 Editor::new("<div>hi</div>\n".as_bytes(), Format::Html).expect("html editor");
3695 let mut md = Editor::new_ext(
3696 ":::div\nhi\n:::\n".as_bytes(),
3697 Format::Markdown,
3698 MarkdownExtensions {
3699 directives: true,
3700 ..Default::default()
3701 },
3702 )
3703 .expect("markdown editor");
3704
3705 let html_nodes = html.nodes().expect("html nodes");
3706 let md_nodes = md.nodes().expect("markdown nodes");
3707 let tag = html_nodes
3708 .iter()
3709 .find(|n| n.name.as_deref() == Some("div"))
3710 .expect("a <div> container");
3711 let directive = md_nodes
3712 .iter()
3713 .find(|n| n.name.as_deref() == Some("div"))
3714 .expect("a :::div container");
3715
3716 assert_eq!(tag.kind, directive.kind);
3718 assert_eq!(tag.name, directive.name);
3719 assert_eq!(tag.directive_form, directive.directive_form);
3720 assert_eq!(tag.directive_form, Some(DirectiveForm::Container));
3721
3722 assert_eq!(tag.origin, Some(ContainerOrigin::Element));
3724 assert_eq!(directive.origin, Some(ContainerOrigin::Directive));
3725 }
3726
3727 fn for_both_formats(src: &str, check: impl Fn(&mut Document, Format)) {
3731 for format in [Format::Markdown, Format::Djot] {
3732 let mut doc = Document::parse(src.as_bytes(), format).expect("parse");
3733 check(&mut doc, format);
3734 }
3735 }
3736
3737 #[test]
3738 fn marker_span_is_what_a_rich_view_hides() {
3739 for_both_formats("> - [x] done\n", |doc, format| {
3740 let nodes = doc.nodes().expect("nodes");
3741 let quote = nodes
3742 .iter()
3743 .find(|n| n.kind == Kind::BlockQuote)
3744 .expect("a block quote");
3745 let item = nodes
3746 .iter()
3747 .find(|n| n.kind == Kind::TaskListItem)
3748 .expect("a task item");
3749
3750 assert_eq!(quote.marker_span, Some(0..2), "{format:?}");
3754 assert_eq!(item.marker_span, Some(2..8), "{format:?}");
3755
3756 assert_eq!(quote.content_span, Some(quote.span.clone()), "{format:?}");
3760
3761 let para = nodes
3763 .iter()
3764 .find(|n| n.kind == Kind::Para)
3765 .expect("a paragraph");
3766 assert_eq!(para.marker_span, None, "{format:?}");
3767 });
3768 }
3769
3770 #[test]
3771 fn line_prefix_assembles_every_marker_on_the_line() {
3772 for_both_formats("> - [x] done\n", |doc, format| {
3773 assert_eq!(doc.line_prefix(9).expect("prefix"), Some(0..8), "{format:?}");
3776 });
3777 }
3778
3779 #[test]
3780 fn line_prefix_is_none_on_a_continuation_line() {
3781 for_both_formats("> c\n> d\n", |doc, format| {
3787 assert_eq!(doc.line_prefix(2).expect("prefix"), Some(0..2), "{format:?}");
3788 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
3789 });
3790 }
3791
3792 #[test]
3793 fn a_caret_at_a_blocks_end_is_in_that_block_in_both_formats() {
3794 for_both_formats("a\n\nb\n", |doc, format| {
3800 for offset in [0usize, 1, 3, 4] {
3801 let hit = doc
3802 .node_at_caret(offset)
3803 .expect("caret hit")
3804 .expect("some node");
3805 assert_eq!(hit.kind, Kind::Str, "{format:?} at {offset}");
3806 }
3807 for offset in [2usize, 5] {
3810 let hit = doc
3811 .node_at_caret(offset)
3812 .expect("caret hit")
3813 .expect("some node");
3814 assert_eq!(hit.kind, Kind::Doc, "{format:?} at {offset}");
3815 }
3816 });
3817 }
3818
3819 #[test]
3820 fn the_caret_chain_ends_at_the_node_the_scalar_call_returns() {
3821 for_both_formats("- a\n", |doc, format| {
3822 let hit = doc.node_at_caret(3).expect("hit").expect("some node");
3823 let chain = doc.ancestors_at_caret(3).expect("chain");
3824 assert_eq!(chain.last().map(|m| m.node_id), Some(hit.node_id), "{format:?}");
3825 assert!(
3828 chain.iter().any(|m| m.kind == Kind::ListItem),
3829 "{format:?}: chain should reach the list item"
3830 );
3831 });
3832 }
3833
3834 #[test]
3835 fn continuation_prefix_repeats_a_quote_and_indents_past_an_item() {
3836 for_both_formats("> - a\n", |doc, format| {
3837 assert_eq!(doc.line_prefix(4).expect("prefix"), Some(0..4), "{format:?}");
3842 let cont = doc.continuation_prefix(4).expect("continuation");
3843 assert_eq!(cont.text, "> ", "{format:?}");
3844 assert_eq!(cont.columns, 4, "{format:?}");
3845 });
3846 }
3847
3848 #[test]
3849 fn continuation_prefix_answers_on_a_line_that_opens_nothing() {
3850 for_both_formats("> c\n> d\n", |doc, format| {
3853 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
3854 assert_eq!(
3855 doc.continuation_prefix(6).expect("continuation").text,
3856 "> ",
3857 "{format:?}"
3858 );
3859 });
3860 }
3861
3862 #[test]
3863 fn continuation_prefix_takes_an_ordered_markers_own_width() {
3864 for_both_formats("10. x\n", |doc, format| {
3867 assert_eq!(
3868 doc.continuation_prefix(4).expect("continuation").columns,
3869 4,
3870 "{format:?}"
3871 );
3872 });
3873 for_both_formats("1. x\n", |doc, format| {
3874 assert_eq!(
3875 doc.continuation_prefix(3).expect("continuation").columns,
3876 3,
3877 "{format:?}"
3878 );
3879 });
3880 }
3881
3882 #[test]
3883 fn a_blank_line_keeps_a_quote_alive_and_drops_an_items_indent() {
3884 for_both_formats("> - a\n", |doc, format| {
3885 let blank = doc.blank_line_prefix(4).expect("blank");
3886 assert_eq!(blank.text, ">", "{format:?}");
3889 assert_eq!(blank.columns, 1, "{format:?}");
3890 });
3891 for_both_formats("- a\n", |doc, format| {
3894 assert_eq!(doc.blank_line_prefix(3).expect("blank").text, "", "{format:?}");
3895 });
3896 }
3897
3898 #[test]
3899 fn a_prefix_column_count_is_not_its_byte_length() {
3900 let mut doc = Document::parse("- x
3903".as_bytes(), Format::Markdown).expect("parse");
3904 let cont = doc.continuation_prefix(2).expect("continuation");
3905 assert_eq!(cont.columns, 4);
3906 }
3907
3908 #[test]
3909 fn set_block_opens_a_heading_on_a_blank_line() {
3910 for format in [Format::Markdown, Format::Djot] {
3911 let mut ed = Editor::new("a\n\n".as_bytes(), format).expect("editor");
3912 ed.set_block(3, BlockKind::Heading(2)).expect("set_block");
3913 assert_eq!(ed.source().expect("source"), b"a\n\n## ", "{format:?}");
3914 let nodes = ed.nodes().expect("nodes");
3918 assert!(
3919 nodes.iter().any(|n| n.kind == Kind::Heading),
3920 "{format:?}: should have parsed a heading"
3921 );
3922 }
3923 }
3924
3925 #[test]
3926 fn set_block_refuses_a_blank_line_inside_a_code_block() {
3927 for format in [Format::Markdown, Format::Djot] {
3931 let src = "```\nx\n\ny\n```\n";
3932 let mut ed = Editor::new(src.as_bytes(), format).expect("editor");
3933 let blank = src.find("\n\n").expect("a blank line") + 1;
3934 assert!(
3935 matches!(
3936 ed.set_block(blank, BlockKind::Heading(1)),
3937 Err(Error::NotEditable)
3938 ),
3939 "{format:?}"
3940 );
3941 assert_eq!(ed.source().expect("source"), src.as_bytes(), "{format:?}");
3942 }
3943 }
3944
3945 #[test]
3946 fn task_items_report_their_checkbox_state() {
3947 for_both_formats("- [ ] a\n- [x] b\n- [X] c\n- d\n", |doc, format| {
3951 let nodes = doc.nodes().expect("nodes");
3952 let states: Vec<Option<bool>> = nodes
3953 .iter()
3954 .filter(|n| matches!(n.kind, Kind::TaskListItem | Kind::ListItem))
3955 .map(|n| n.checked)
3956 .collect();
3957 assert_eq!(
3958 states,
3959 vec![Some(false), Some(true), Some(true), None],
3960 "{format:?}"
3961 );
3962
3963 for n in nodes.iter().filter(|n| n.kind == Kind::Para) {
3966 assert_eq!(n.checked, None, "{format:?}");
3967 }
3968 });
3969 }
3970
3971 #[test]
3972 fn an_editor_reaches_the_caret_reads_through_its_document_view() {
3973 let mut ed = Editor::new("- a\n".as_bytes(), Format::Markdown).expect("editor");
3978 let mut view = ed.document().expect("document view");
3979
3980 assert_eq!(view.line_prefix(3).expect("prefix"), Some(0..2));
3981 let hit = view.node_at_caret(3).expect("hit").expect("some node");
3982 assert_eq!(hit.kind, Kind::Str);
3983 }
3984
3985 #[test]
3986 fn container_origin_is_none_for_non_containers() {
3987 let mut ed = Editor::new("# hi\n\npara\n".as_bytes(), Format::Markdown).expect("editor");
3990 for n in ed.nodes().expect("nodes") {
3991 assert_eq!(n.origin, None, "{} should carry no origin", n.kind);
3992 }
3993 }
3994
3995 #[test]
3996 fn flat_nodes_expose_directive_name_and_form() {
3997 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
4003 let mut ed = Editor::new_ext(
4004 src.as_bytes(),
4005 Format::Markdown,
4006 MarkdownExtensions {
4007 directives: true,
4008 ..Default::default()
4009 },
4010 )
4011 .expect("editor");
4012 let nodes = ed.nodes().expect("nodes");
4013
4014 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
4015 .iter()
4016 .filter(|n| n.kind == Kind::Container)
4017 .map(|n| (n.name.as_deref(), n.directive_form))
4018 .collect();
4019 assert_eq!(
4020 forms,
4021 vec![
4022 (Some("note"), Some(DirectiveForm::Container)),
4023 (Some("embed"), Some(DirectiveForm::Leaf)),
4024 (Some("abbr"), Some(DirectiveForm::Text)),
4025 ]
4026 );
4027
4028 let embed = nodes
4031 .iter()
4032 .find(|n| n.name.as_deref() == Some("embed"))
4033 .expect("embed");
4034 assert_eq!(
4035 embed.attrs,
4036 vec![("src".to_string(), Some("demo.html".to_string()))]
4037 );
4038 let para = nodes.iter().find(|n| n.kind == Kind::Para).expect("a para");
4039 assert!(para.directive_form.is_none() && para.name.is_none());
4040 }
4041
4042 #[test]
4043 fn editor_insert_child_and_delete() {
4044 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
4045 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4046 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
4047 ed.delete("0.1").expect("delete");
4048 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
4049 }
4050
4051 #[test]
4052 fn editor_edits_by_selector() {
4053 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4054 ed.replace("heading(\"Two\")", "## Renamed")
4055 .expect("replace");
4056 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
4057 }
4058
4059 #[test]
4060 fn editor_locator_errors_are_distinct() {
4061 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
4062 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
4063 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
4064 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
4065 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
4067 }
4068
4069 #[test]
4070 fn editor_reparse_break_rolls_back() {
4071 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
4072 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
4073 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
4074 }
4075
4076 #[test]
4077 fn editor_leaf_content_is_not_editable() {
4078 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4079 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
4080 }
4081
4082 #[test]
4083 fn editor_query_reflects_current_tree() {
4084 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
4085 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4086 assert_eq!(ed.query("element").expect("query").len(), 3);
4088 let json = ed.ast_json().expect("ast_json");
4089 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
4090 }
4091
4092 #[test]
4095 fn editor_edit_range_types_backspaces_and_reports_change() {
4096 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4097
4098 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
4100 assert_eq!(ed.source_str().unwrap(), "aXb\n");
4101 assert_eq!(c.old, 1..1);
4102 assert_eq!(c.new, 1..2);
4103 assert_eq!(c.delta(), 1);
4104
4105 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
4107 assert_eq!(ed.source_str().unwrap(), "ab\n");
4108 assert_eq!(c2.old, 1..2);
4109 assert_eq!(c2.new, 1..1);
4110 assert_eq!(c2.delta(), -1);
4111 }
4112
4113 #[test]
4114 fn editor_edit_range_rejects_bad_ranges() {
4115 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4116 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"); }
4120
4121 #[test]
4122 fn editor_last_change_reports_locator_ops_too() {
4123 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4124 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed")
4127 .expect("replace");
4128 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
4129 let c = ed.last_change().expect("a change was recorded");
4130 assert_eq!(c.old, 7..13);
4132 assert_eq!(c.new, 7..17);
4133 }
4134
4135 #[test]
4136 fn editor_nodes_is_a_walkable_flat_tree() {
4137 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4138 let nodes = ed.nodes().expect("nodes");
4139 assert!(!nodes.is_empty());
4140
4141 for (i, n) in nodes.iter().enumerate() {
4143 assert_eq!(n.id, NodeId(i as u32));
4144 }
4145 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
4147 assert_eq!(roots.len(), 1);
4148 assert_eq!(roots[0].kind, Kind::Doc);
4149
4150 let heading = nodes
4152 .iter()
4153 .find(|n| n.kind == Kind::Heading)
4154 .expect("a heading");
4155 assert_eq!(heading.level, Some(1));
4156 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
4157
4158 assert_eq!(heading.head, None);
4160 assert_eq!(heading.alignment, None);
4161
4162 for n in nodes.iter().filter(|n| n.parent.is_some()) {
4165 let p = &nodes[n.parent.unwrap().0 as usize];
4166 let mut kid = p.first_child;
4167 let mut seen = false;
4168 while let Some(NodeId(k)) = kid {
4169 if k == n.id.0 {
4170 seen = true;
4171 break;
4172 }
4173 kid = nodes[k as usize].next_sibling;
4174 }
4175 assert!(
4176 seen,
4177 "node {:?} not found among its parent's children",
4178 n.id
4179 );
4180 }
4181 }
4182
4183 #[test]
4184 fn editor_child_spans_and_subtree_agree_with_nodes() {
4185 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
4186 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4187 let all = ed.nodes().expect("nodes");
4188 let doc = all.iter().find(|n| n.kind == Kind::Doc).expect("doc");
4189
4190 let top = ed.child_spans(None).expect("child_spans");
4193 let mut want = Vec::new();
4194 let mut c = doc.first_child;
4195 while let Some(id) = c {
4196 want.push(id);
4197 c = all[id.0 as usize].next_sibling;
4198 }
4199 assert_eq!(top.len(), want.len(), "top-level count");
4200 for (m, id) in top.iter().zip(&want) {
4201 assert_eq!(m.node_id, id.0, "child id");
4202 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
4203 assert_eq!(m.span, all[id.0 as usize].span, "child span");
4204 }
4205 assert!(
4207 src[top[0].span.clone()].starts_with('#'),
4208 "first block is the heading"
4209 );
4210
4211 let list = top
4213 .iter()
4214 .find(|m| {
4215 matches!(
4216 m.kind,
4217 Kind::BulletList | Kind::OrderedList | Kind::TaskList
4218 )
4219 })
4220 .expect("a list");
4221 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
4222 assert_eq!(items.len(), 2);
4223 assert!(
4224 items.iter().all(|m| m.kind == Kind::ListItem),
4225 "items: {items:?}"
4226 );
4227
4228 let para = top
4230 .iter()
4231 .find(|m| m.kind == Kind::Para)
4232 .expect("a para")
4233 .node_id;
4234 let sub = ed.subtree(NodeId(para)).expect("subtree");
4235 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
4236 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
4237 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
4238 assert_eq!(sub[0].kind, Kind::Para);
4239 for (i, n) in sub.iter().enumerate() {
4240 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
4241 for link in [n.parent, n.first_child, n.next_sibling]
4242 .into_iter()
4243 .flatten()
4244 {
4245 assert!(
4246 (link.0 as usize) < sub.len(),
4247 "link {link:?} escapes the subtree"
4248 );
4249 }
4250 }
4251 assert!(
4252 src[sub[0].span.clone()].starts_with("Hello"),
4253 "absolute span: {:?}",
4254 &src[sub[0].span.clone()]
4255 );
4256
4257 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<Kind> {
4259 let mut out = Vec::new();
4260 let mut stack = vec![root];
4261 while let Some(id) = stack.pop() {
4262 let n = &all[id.0 as usize];
4263 out.push(n.kind.clone());
4264 let mut c = n.first_child;
4265 while let Some(cid) = c {
4266 stack.push(cid);
4267 c = all[cid.0 as usize].next_sibling;
4268 }
4269 }
4270 out
4271 }
4272 let mut want_kinds = arena_kinds(&all, NodeId(para));
4273 let mut got_kinds: Vec<Kind> = sub.iter().map(|n| n.kind.clone()).collect();
4274 want_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4278 got_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4279 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
4280
4281 assert!(matches!(
4283 ed.subtree(NodeId(9999)),
4284 Err(Error::InvalidArgument)
4285 ));
4286 }
4287
4288 #[test]
4289 fn flat_nodes_carry_table_head_and_alignment() {
4290 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
4294 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4295 let nodes = ed.nodes().expect("nodes");
4296
4297 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Row).collect();
4298 assert_eq!(rows.len(), 2, "a header row and one body row");
4299 assert_eq!(rows[0].head, Some(true), "first row is the header");
4300 assert_eq!(rows[1].head, Some(false), "second row is a body row");
4301
4302 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Cell).collect();
4303 assert_eq!(cells.len(), 4);
4304 assert_eq!(cells[0].alignment, Some(Alignment::Left));
4306 assert_eq!(cells[1].alignment, Some(Alignment::Right));
4307 assert_eq!(cells[2].alignment, Some(Alignment::Left));
4308 assert_eq!(cells[3].alignment, Some(Alignment::Right));
4309 assert_eq!(cells[0].head, Some(true));
4311 assert_eq!(cells[2].head, Some(false));
4312
4313 let mut plain =
4316 Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
4317 let pnodes = plain.nodes().expect("nodes");
4318 let pcell = pnodes
4319 .iter()
4320 .find(|n| n.kind == Kind::Cell)
4321 .expect("a cell");
4322 assert_eq!(pcell.alignment, Some(Alignment::Default));
4323 }
4324
4325 #[test]
4326 fn cell_extent_reports_merged_cells_and_nothing_else() {
4327 let src = "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td><td>b</td></tr></table>";
4328 let mut doc = Document::parse_str(src, Format::Html).expect("parse");
4329 let cells: Vec<NodeId> = doc
4330 .nodes()
4331 .expect("nodes")
4332 .iter()
4333 .filter(|n| n.kind == Kind::Cell)
4334 .map(|n| n.id)
4335 .collect();
4336 assert_eq!(cells.len(), 2);
4337 assert_eq!(doc.cell_extent(cells[0]).expect("extent"), Some((2, 3)));
4338 assert_eq!(doc.cell_extent(cells[1]).expect("extent"), Some((1, 1)));
4340
4341 let mut pipe =
4343 Document::parse_str("| a |\n| --- |\n| b |\n", Format::Markdown).expect("parse");
4344 let pipe_cell = pipe
4345 .nodes()
4346 .expect("nodes")
4347 .iter()
4348 .find(|n| n.kind == Kind::Cell)
4349 .expect("a cell")
4350 .id;
4351 assert_eq!(pipe.cell_extent(pipe_cell).expect("extent"), Some((1, 1)));
4352
4353 let root = NodeId(0);
4355 assert_eq!(pipe.cell_extent(root).expect("extent"), None);
4356 }
4357
4358 #[test]
4359 fn builder_add_cell_spanning_renders_colspan_and_rowspan() {
4360 let mut b = Builder::new().expect("builder");
4361 let wide_text = b.add_text(TextKind::Str, "wide").expect("str");
4362 let wide = b
4363 .add_cell_spanning(false, Alignment::Default, 2, 3)
4364 .expect("cell");
4365 b.set_children(wide, &[wide_text]).expect("children");
4366 let plain_text = b.add_text(TextKind::Str, "one").expect("str");
4367 let plain = b.add_cell(false, Alignment::Default).expect("cell");
4368 b.set_children(plain, &[plain_text]).expect("children");
4369 let row = b.add_row(false).expect("row");
4370 b.set_children(row, &[wide, plain]).expect("children");
4371 let table = b.add(VoidKind::Table).expect("table");
4372 b.set_children(table, &[row]).expect("children");
4373
4374 let html = String::from_utf8(b.render_html(table).expect("html")).expect("utf-8");
4375 assert!(
4376 html.contains("<td colspan=\"2\" rowspan=\"3\">wide</td>"),
4377 "{html}"
4378 );
4379 assert!(html.contains("<td>one</td>"), "{html}");
4381
4382 assert!(matches!(
4384 b.add_cell_spanning(false, Alignment::Default, 0, 1),
4385 Err(Error::InvalidArgument)
4386 ));
4387 }
4388
4389 #[test]
4390 fn editor_node_at_and_ancestors_hit_test_offsets() {
4391 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4392
4393 let m = ed
4395 .node_at(2)
4396 .expect("node_at")
4397 .expect("a node covers offset 2");
4398 assert!(m.span.contains(&2));
4399
4400 let chain = ed.ancestors_at(2).expect("ancestors_at");
4402 assert!(!chain.is_empty());
4403 assert_eq!(chain[0].kind, Kind::Doc);
4404 assert_eq!(chain.last().unwrap().node_id, m.node_id);
4405
4406 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
4408 }
4409
4410 #[test]
4413 fn editor_wrap_and_toggle_inline_round_trip() {
4414 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4415
4416 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
4418 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
4419 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
4420
4421 ed.toggle_inline(4, 8, InlineKind::Strong)
4423 .expect("toggle off");
4424 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4425
4426 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
4428 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
4429 }
4430
4431 #[test]
4432 fn editor_inline_kind_support_is_format_specific() {
4433 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4435 assert_eq!(
4436 md.wrap_range(2, 6, InlineKind::Mark),
4437 Err(Error::UnsupportedFormat)
4438 );
4439
4440 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4442 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
4443 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
4444 }
4445
4446 #[test]
4447 fn editor_toggle_strips_verbatim_via_content_span() {
4448 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
4449 ed.toggle_inline(2, 8, InlineKind::Verbatim)
4451 .expect("toggle code off");
4452 assert_eq!(ed.source_str().unwrap(), "a code b\n");
4453
4454 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
4457 ed2.toggle_inline(2, 7, InlineKind::Verbatim)
4458 .expect("toggle multi off");
4459 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
4460 }
4461
4462 #[test]
4463 fn editor_set_block_switches_para_and_heading_levels() {
4464 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
4465
4466 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
4468 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
4469
4470 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
4472 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
4473
4474 ed.set_block(2, BlockKind::Paragraph).expect("to para");
4476 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
4477 }
4478
4479 #[test]
4480 fn editor_set_block_rejects_bad_level_and_format() {
4481 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4482 assert_eq!(
4483 md.set_block(0, BlockKind::Heading(9)),
4484 Err(Error::InvalidArgument)
4485 );
4486
4487 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4488 assert_eq!(
4489 xml.set_block(1, BlockKind::Heading(1)),
4490 Err(Error::UnsupportedFormat)
4491 );
4492 }
4493
4494 #[test]
4495 fn editor_toggle_block_container_round_trips() {
4496 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
4497
4498 let c = ed
4499 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
4500 .expect("quote on");
4501 assert_eq!(ed.source_str().unwrap(), "> a\n");
4502 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
4503
4504 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4505 .expect("quote off");
4506 assert_eq!(ed.source_str().unwrap(), "a\n");
4507 }
4508
4509 #[test]
4510 fn editor_toggle_block_container_nests_a_partial_selection() {
4511 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
4512
4513 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4516 .expect("nest");
4517 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
4518
4519 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
4521 .expect("peel");
4522 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
4523 }
4524
4525 #[test]
4526 fn editor_toggle_block_container_numbers_and_converts_lists() {
4527 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
4528
4529 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
4531 .expect("ordered on");
4532 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
4533
4534 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
4536 .expect("convert");
4537 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
4538 }
4539
4540 #[test]
4541 fn editor_toggle_block_container_rejects_unspellable_format() {
4542 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4543 assert_eq!(
4544 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
4545 Err(Error::UnsupportedFormat)
4546 );
4547 }
4548
4549 #[test]
4550 fn editor_insert_link_wraps_and_repoints() {
4551 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4552
4553 ed.insert_link(2, 6, "http://x.dev").expect("link");
4554 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
4555
4556 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
4558 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
4559 }
4560
4561 #[test]
4562 fn editor_insert_link_repoints_an_autolink() {
4563 for format in [Format::Markdown, Format::Djot] {
4568 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
4569 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
4570 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
4571
4572 let nodes = ed.nodes().expect("nodes");
4574 let url = nodes
4575 .iter()
4576 .find(|n| n.kind == Kind::Url)
4577 .expect("still an autolink");
4578 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
4579 assert!(!nodes.iter().any(|n| n.kind == Kind::Link));
4580 }
4581 }
4582
4583 #[test]
4584 fn editor_insert_link_escapes_the_destination() {
4585 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4588 dj.insert_link(0, 1, "a)b").expect("link");
4589 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
4590
4591 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4595 md.insert_link(0, 1, "a b").expect("link");
4596 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
4597
4598 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
4599 dj2.insert_link(0, 1, "a b").expect("link");
4600 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
4601 }
4602
4603 #[test]
4604 fn editor_insert_image_escapes_the_destination_per_format() {
4605 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4608 md.insert_image(0, 1, "my cat.png").expect("image");
4609 assert_eq!(md.source_str().unwrap(), "\n");
4610
4611 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4612 dj.insert_image(0, 1, "my cat.png").expect("image");
4613 assert_eq!(dj.source_str().unwrap(), "\n");
4614
4615 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
4617 paren.insert_image(0, 1, "a)b.png").expect("image");
4618 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
4619 }
4620
4621 #[test]
4622 fn editor_insert_image_keeps_an_empty_alt_empty() {
4623 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4626 ed.insert_image(1, 1, "cat.png").expect("image");
4627 assert_eq!(ed.source_str().unwrap(), "ab\n");
4628 }
4629
4630 #[test]
4631 fn editor_insert_image_rejects_a_newline_destination() {
4632 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
4633 assert_eq!(
4634 ed.insert_image(0, 1, "a\nb.png"),
4635 Err(Error::InvalidArgument)
4636 );
4637
4638 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4639 assert_eq!(
4640 xml.insert_image(3, 5, "x.png"),
4641 Err(Error::UnsupportedFormat)
4642 );
4643 }
4644
4645 #[test]
4646 fn editor_insert_link_rejects_a_newline_destination() {
4647 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
4648 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
4649
4650 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4651 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
4652 }
4653
4654 #[test]
4655 fn editor_insert_literal_keeps_typed_specials_literal() {
4656 for format in [Format::Markdown, Format::Djot] {
4657 let mut ed = Editor::new_str("z\n", format).expect("editor");
4658 ed.insert_literal(0, "*hi*").expect("literal");
4660
4661 let nodes = ed.nodes().expect("nodes");
4663 assert!(
4664 !nodes
4665 .iter()
4666 .any(|n| n.kind == Kind::Emph || n.kind == Kind::Strong)
4667 );
4668 let text: String = nodes
4669 .iter()
4670 .filter(|n| n.kind == Kind::Str)
4671 .filter_map(|n| n.text.clone())
4672 .collect();
4673 assert_eq!(text, "*hi*z");
4674 }
4675 }
4676
4677 #[test]
4678 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
4679 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
4681 ed.insert_literal(1, "# ").expect("literal");
4682 assert_eq!(ed.source_str().unwrap(), "a# z\n");
4683
4684 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
4686 ed2.insert_literal(0, "# ").expect("literal");
4687 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
4688 assert!(
4689 !ed2.nodes()
4690 .expect("nodes")
4691 .iter()
4692 .any(|n| n.kind == Kind::Heading)
4693 );
4694 }
4695
4696 #[test]
4697 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
4698 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4699 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
4700
4701 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4702 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
4703 }
4704
4705 #[test]
4706 fn editor_insert_line_break_splices_in_cell_br() {
4707 let mut ed =
4708 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
4709 ed.insert_line_break(3).expect("line break");
4711 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
4712 let nodes = ed.nodes().expect("nodes");
4714 assert!(nodes.iter().any(|n| n.kind == Kind::HardBreak));
4715 assert!(!nodes.iter().any(|n| n.kind == Kind::RawInline));
4716 }
4717
4718 #[test]
4719 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
4720 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
4722 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
4723
4724 let mut dj = Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
4726 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
4727
4728 let mut ed =
4730 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
4731 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
4732 }
4733
4734 #[test]
4735 fn editor_insert_thematic_break_is_blank_separated_per_format() {
4736 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
4740 md.insert_thematic_break(0).expect("rule");
4741 assert_eq!(md.source_str().unwrap(), "a\n\n---\n");
4742 let nodes = md.nodes().expect("nodes");
4743 assert!(nodes.iter().any(|n| n.kind == Kind::ThematicBreak));
4744 assert!(!nodes.iter().any(|n| n.kind == Kind::Heading));
4745
4746 let mut dj = Editor::new_str("a\n", Format::Djot).expect("editor");
4749 dj.insert_thematic_break(0).expect("rule");
4750 assert_eq!(dj.source_str().unwrap(), "a\n\n* * *\n");
4751
4752 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4753 assert_eq!(xml.insert_thematic_break(3), Err(Error::UnsupportedFormat));
4754 }
4755
4756 #[test]
4757 fn editor_split_block_keeps_both_halves_the_same_kind() {
4758 let mut item = Editor::new_str("- this is a list item\n", Format::Markdown).expect("editor");
4761 item.split_block(10).expect("split");
4762 assert_eq!(item.source_str().unwrap(), "- this is \n- a list item\n");
4763 let nodes = item.nodes().expect("nodes");
4764 assert_eq!(nodes.iter().filter(|n| n.kind == Kind::ListItem).count(), 2);
4765
4766 let mut tail = Editor::new_str("- a\n", Format::Markdown).expect("editor");
4768 tail.split_block(3).expect("split");
4769 assert_eq!(tail.source_str().unwrap(), "- a\n- \n");
4770
4771 let mut para = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4773 para.split_block(1).expect("split");
4774 assert_eq!(para.source_str().unwrap(), "a\n\nb\n");
4775
4776 let mut table =
4778 Editor::new_str("| a | b |\n|---|---|\n| c | d |\n", Format::Markdown).expect("editor");
4779 assert_eq!(table.split_block(3), Err(Error::NotEditable));
4780
4781 let mut empty = Editor::new_str("", Format::Markdown).expect("editor");
4782 assert_eq!(empty.split_block(0), Err(Error::NotFound));
4783 }
4784
4785 #[test]
4786 fn editor_toggle_code_block_round_trips_and_measures_the_fence() {
4787 let mut ed = Editor::new_str("a\n", Format::Markdown).expect("editor");
4788 ed.toggle_code_block(0, 1, Some("zig")).expect("fence");
4789 assert_eq!(ed.source_str().unwrap(), "```zig\na\n```\n");
4790 let nodes = ed.nodes().expect("nodes");
4791 assert!(nodes.iter().any(|n| n.kind == Kind::CodeBlock));
4792
4793 ed.toggle_code_block(0, 0, None).expect("unfence");
4794 assert_eq!(ed.source_str().unwrap(), "a\n");
4795
4796 let mut runs = Editor::new_str("a ``` b\n", Format::Markdown).expect("editor");
4799 runs.toggle_code_block(0, 7, None).expect("fence");
4800 assert_eq!(runs.source_str().unwrap(), "````\na ``` b\n````\n");
4801 }
4802
4803 #[test]
4804 fn editor_toggle_code_block_refuses_inside_a_list_item() {
4805 let mut ed = Editor::new_str("- a\n- b\n", Format::Markdown).expect("editor");
4808 assert_eq!(ed.toggle_code_block(2, 3, None), Err(Error::NotEditable));
4809 assert_eq!(ed.source_str().unwrap(), "- a\n- b\n");
4810 }
4811
4812 #[test]
4813 fn editor_set_code_language_retags_clears_and_refuses() {
4814 let mut ed = Editor::new_str("```zig\na\n```\n", Format::Markdown).expect("editor");
4815 ed.set_code_language(0, Some("rust")).expect("retag");
4816 assert_eq!(ed.source_str().unwrap(), "```rust\na\n```\n");
4817
4818 ed.set_code_language(0, None).expect("clear");
4821 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
4822 ed.set_code_language(0, Some("")).expect("empty");
4823 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
4824
4825 assert_eq!(
4828 ed.set_code_language(0, Some("a b")),
4829 Err(Error::InvalidArgument)
4830 );
4831 let mut dj = Editor::new_str("```\na\n```\n", Format::Djot).expect("editor");
4833 dj.set_code_language(0, Some("a b"))
4834 .expect("djot info string");
4835 assert_eq!(dj.source_str().unwrap(), "```a b\na\n```\n");
4836
4837 let mut para = Editor::new_str("x\n", Format::Markdown).expect("editor");
4838 assert_eq!(para.set_code_language(0, Some("zig")), Err(Error::NotFound));
4839 }
4840
4841 #[test]
4842 fn editor_task_checkbox_gestures() {
4843 let mut ed = Editor::new_str("- a\n", Format::Markdown).expect("editor");
4844
4845 ed.toggle_task_item(2).expect("add box");
4848 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
4849 assert!(
4850 ed.nodes()
4851 .unwrap()
4852 .iter()
4853 .any(|n| n.kind == Kind::TaskListItem)
4854 );
4855
4856 ed.set_task_checked(6, true).expect("tick");
4857 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
4858 ed.set_task_checked(6, true).expect("no-op");
4860 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
4861
4862 ed.toggle_task_checked(6).expect("flip");
4863 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
4864
4865 ed.toggle_task_item(6).expect("remove box");
4866 assert_eq!(ed.source_str().unwrap(), "- a\n");
4867
4868 assert_eq!(ed.set_task_checked(2, true), Err(Error::NotEditable));
4871 let mut para = Editor::new_str("a\n", Format::Markdown).expect("editor");
4873 assert_eq!(para.toggle_task_item(0), Err(Error::NotFound));
4874 }
4875
4876 #[test]
4877 fn editor_insert_footnote_writes_both_halves_as_one_edit() {
4878 for format in [Format::Markdown, Format::Djot] {
4879 let mut ed = Editor::new_str("see\n", format).expect("editor");
4880 ed.insert_footnote(3, "a").expect("footnote");
4881 assert_eq!(ed.source_str().unwrap(), "see[^a]\n\n[^a]: \n");
4882
4883 let nodes = ed.nodes().expect("nodes");
4885 assert!(nodes.iter().any(|n| n.kind == Kind::FootnoteReference));
4886 assert!(nodes.iter().any(|n| n.kind == Kind::Footnote));
4887
4888 ed.undo().expect("undo");
4890 assert_eq!(ed.source_str().unwrap(), "see\n");
4891 }
4892 }
4893
4894 #[test]
4895 fn editor_insert_footnote_reuses_an_existing_definition() {
4896 let mut ed = Editor::new_str("see\n", Format::Markdown).expect("editor");
4897 ed.insert_footnote(3, "a").expect("first");
4898 ed.insert_footnote(7, "a").expect("second reference");
4899 assert_eq!(ed.source_str().unwrap(), "see[^a][^a]\n\n[^a]: \n");
4900 let defs = ed
4901 .nodes()
4902 .unwrap()
4903 .iter()
4904 .filter(|n| n.kind == Kind::Footnote)
4905 .count();
4906 assert_eq!(defs, 1);
4907
4908 assert_eq!(ed.insert_footnote(3, ""), Err(Error::InvalidArgument));
4909 assert_eq!(ed.insert_footnote(3, "a]b"), Err(Error::InvalidArgument));
4910 }
4911
4912 #[test]
4913 fn editor_undo_redo_round_trip() {
4914 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
4915 ed.edit_range(5, 5, "!").expect("edit");
4916 assert_eq!(ed.source_str().unwrap(), "hello!\n");
4917
4918 let change = ed.undo().expect("undo ok").expect("something to undo");
4919 assert_eq!(ed.source_str().unwrap(), "hello\n");
4920 assert_eq!(change.new.end, 5);
4921 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
4922
4923 ed.redo().expect("redo ok").expect("something to redo");
4924 assert_eq!(ed.source_str().unwrap(), "hello!\n");
4925 }
4926
4927 #[test]
4928 fn editor_coalesce_folds_a_run() {
4929 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
4930 ed.edit_range(0, 0, "a").expect("edit");
4931 ed.edit_range(1, 1, "b").expect("edit");
4932 ed.coalesce_last_undo().expect("coalesce");
4933 assert_eq!(ed.source_str().unwrap(), "ab\n");
4934 ed.undo().expect("undo ok").expect("something to undo");
4936 assert_eq!(ed.source_str().unwrap(), "\n");
4937 assert!(ed.undo().expect("undo ok").is_none());
4938 }
4939
4940 #[test]
4941 fn editor_revision_bumps_per_successful_mutation() {
4942 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
4943 assert_eq!(ed.revision(), 0);
4944 ed.edit_range(1, 1, "y").expect("edit");
4945 assert_eq!(ed.revision(), 1);
4946
4947 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
4949 assert_eq!(xml.revision(), 0);
4950 assert!(xml.replace_content("0", "<b>").is_err());
4951 assert_eq!(xml.revision(), 0);
4952
4953 ed.undo().expect("undo ok").expect("something to undo");
4955 assert_eq!(ed.revision(), 2);
4956 ed.redo().expect("redo ok").expect("something to redo");
4957 assert_eq!(ed.revision(), 3);
4958 }
4959
4960 #[test]
4961 fn editor_dirty_range_tracks_and_clears() {
4962 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
4963 assert_eq!(ed.dirty_range(), None);
4965
4966 ed.edit_range(2, 2, "XY").expect("edit");
4968 assert_eq!(ed.dirty_range(), Some(2..4));
4969
4970 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
4974 assert!(
4975 d.start <= 2 && d.end >= 10,
4976 "range {d:?} must cover both edits"
4977 );
4978
4979 let rev = ed.revision();
4981 ed.clear_dirty();
4982 assert_eq!(ed.dirty_range(), None);
4983 assert_eq!(ed.revision(), rev);
4984
4985 ed.undo().expect("undo ok").expect("something to undo");
4987 assert!(ed.dirty_range().is_some());
4988 }
4989
4990 #[test]
4991 fn editor_caret_blob_follows_undo_and_redo() {
4992 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
4993 assert!(ed.caret_blob().unwrap().is_empty());
4994
4995 ed.set_caret_blob(b"before").expect("set caret");
4997 ed.edit_range(5, 5, "!").expect("edit");
4998 assert!(ed.caret_blob().unwrap().is_empty());
5000 ed.set_caret_blob(b"after").expect("set caret");
5001
5002 ed.undo().expect("undo ok").expect("something to undo");
5004 assert_eq!(ed.source_str().unwrap(), "hello\n");
5005 assert_eq!(ed.caret_blob().unwrap(), b"before");
5006
5007 ed.redo().expect("redo ok").expect("something to redo");
5009 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5010 assert_eq!(ed.caret_blob().unwrap(), b"after");
5011 }
5012
5013 #[test]
5014 fn editor_coalesced_run_keeps_the_pre_run_caret() {
5015 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5016 ed.set_caret_blob(b"c0").expect("set caret");
5017 ed.edit_range(0, 0, "a").expect("edit");
5018 ed.set_caret_blob(b"c1").expect("set caret");
5019 ed.edit_range(1, 1, "b").expect("edit");
5020 ed.coalesce_last_undo().expect("coalesce");
5021 ed.set_caret_blob(b"c2").expect("set caret");
5022
5023 ed.undo().expect("undo ok").expect("something to undo");
5025 assert_eq!(ed.source_str().unwrap(), "\n");
5026 assert_eq!(ed.caret_blob().unwrap(), b"c0");
5027 }
5028
5029 #[test]
5030 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
5031 let mut ed = Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
5032 ed.renumber_ordered_lists(0).expect("renumber ok");
5033 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
5034 }
5035
5036 #[test]
5037 fn editor_renumber_ordered_lists_leaves_djot_prose_alone() {
5038 let src = "1. a\n 2. b\n2. c\n";
5041 let mut dj = Editor::new_str(src, Format::Djot).expect("editor");
5042 dj.renumber_ordered_lists(0).expect("renumber ok");
5043 assert_eq!(dj.source_str().unwrap(), src);
5044
5045 let mut md = Editor::new_str(src, Format::Markdown).expect("editor");
5046 md.renumber_ordered_lists(0).expect("renumber ok");
5047 assert_eq!(md.source_str().unwrap(), "1. a\n 1. b\n2. c\n");
5048 }
5049
5050 #[test]
5051 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
5052 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
5053 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
5054 }
5055
5056 #[test]
5057 fn editor_table_insert_row_and_set_alignment() {
5058 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
5059 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
5060 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
5062 ed.source_str().unwrap(),
5063 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
5064 );
5065 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
5067 }
5068
5069 #[test]
5070 fn editor_table_edit_off_a_table_is_not_found() {
5071 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
5072 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
5073 }
5074
5075 #[test]
5076 fn editor_set_block_converts_setext_heading() {
5077 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
5079 ed.set_block(0, BlockKind::Heading(1))
5080 .expect("setext to atx");
5081 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
5082 }
5083
5084 #[test]
5085 fn editor_unwrap_and_smart_delete() {
5086 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
5087 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
5089
5090 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
5091 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
5093 }
5094
5095 #[test]
5096 fn editor_directives_require_the_extension_flag() {
5097 let src = ":::vis{.public}\nhi\n:::\n";
5098 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
5101 assert_eq!(plain.query("directive").expect("query").len(), 0);
5102 let mut ext = Editor::new_ext(
5104 src.as_bytes(),
5105 Format::Markdown,
5106 MarkdownExtensions {
5107 directives: true,
5108 ..Default::default()
5109 },
5110 )
5111 .expect("editor");
5112 assert_eq!(ext.query("directive").expect("query").len(), 1);
5113 }
5114
5115 #[test]
5116 fn document_html_elements_make_embedded_img_queryable() {
5117 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
5118 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
5120 assert_eq!(plain.query("image").expect("query").len(), 0);
5121 let mut ext = Document::parse_str_with(
5123 src,
5124 Format::Markdown,
5125 MarkdownExtensions {
5126 html_elements: true,
5127 ..Default::default()
5128 },
5129 )
5130 .expect("parse");
5131 let images = ext.query("image").expect("query");
5132 assert_eq!(images.len(), 1);
5133 assert_eq!(images[0].kind, Kind::Image);
5134 }
5135
5136 #[test]
5137 fn editor_filter_public_audience_view() {
5138 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
5139 let mut ed = Editor::new_ext(
5140 src.as_bytes(),
5141 Format::Markdown,
5142 MarkdownExtensions {
5143 directives: true,
5144 ..Default::default()
5145 },
5146 )
5147 .expect("editor");
5148 ed.filter(
5150 "directive[name=vis]",
5151 Some("directive[class~=public]"),
5152 true,
5153 )
5154 .expect("filter");
5155 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
5156 }
5157
5158 #[test]
5159 fn editor_filter_rejects_a_malformed_selector() {
5160 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
5161 assert_eq!(
5162 ed.filter("list >", None, false),
5163 Err(Error::InvalidArgument)
5164 );
5165 }
5166
5167 #[test]
5168 fn builder_builds_and_renders_a_document() {
5169 let mut b = Builder::new().expect("builder");
5170
5171 let title = b.add_text(TextKind::Str, "Title").unwrap();
5173 let heading = b.add_heading(1).unwrap();
5174 b.set_children(heading, &[title]).unwrap();
5175
5176 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
5177 let world = b.add_text(TextKind::Str, "world").unwrap();
5178 let emph = b.add(VoidKind::Emph).unwrap();
5179 b.set_children(emph, &[world]).unwrap();
5180 let para = b.add(VoidKind::Para).unwrap();
5181 b.set_children(para, &[hello, emph]).unwrap();
5182
5183 let doc = b.add(VoidKind::Doc).unwrap();
5184 b.set_children(doc, &[heading, para]).unwrap();
5185
5186 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
5187 assert!(html.contains("<h1>Title</h1>"), "{html}");
5188 assert!(html.contains("<em>world</em>"), "{html}");
5189
5190 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5191 assert!(md.contains("# Title"), "{md}");
5192 assert!(md.contains("*world*"), "{md}");
5193
5194 let matches = b.query(doc, "heading").unwrap();
5195 assert_eq!(matches.len(), 1);
5196 assert_eq!(matches[0].kind, Kind::Heading);
5197
5198 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
5199 assert!(json.contains("\"kind\": \"doc\""), "{json}");
5200 }
5201
5202 #[test]
5203 fn builder_element_with_attributes() {
5204 let mut b = Builder::new().expect("builder");
5205 let inner = b.add_text(TextKind::Str, "hi").unwrap();
5206 let el = b.add_element("section").unwrap();
5207 b.set_children(el, &[inner]).unwrap();
5208 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)])
5209 .unwrap();
5210
5211 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
5212 assert!(html.contains("<section"), "{html}");
5213 assert!(html.contains("class=\"note\""), "{html}");
5214 assert!(html.contains("hidden"), "{html}");
5215 }
5216
5217 #[test]
5218 fn builder_lists_round_trip_to_markdown() {
5219 let mut b = Builder::new().expect("builder");
5220
5221 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
5223 let one_para = b.add(VoidKind::Para).unwrap();
5224 b.set_children(one_para, &[one_txt]).unwrap();
5225 let one = b.add(VoidKind::ListItem).unwrap();
5226 b.set_children(one, &[one_para]).unwrap();
5227
5228 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
5229 let two_para = b.add(VoidKind::Para).unwrap();
5230 b.set_children(two_para, &[two_txt]).unwrap();
5231 let two = b.add(VoidKind::ListItem).unwrap();
5232 b.set_children(two, &[two_para]).unwrap();
5233
5234 let list = b
5235 .add_ordered_list(
5236 OrderedNumbering::Decimal,
5237 OrderedDelim::Period,
5238 true,
5239 Some(1),
5240 )
5241 .unwrap();
5242 b.set_children(list, &[one, two]).unwrap();
5243 let doc = b.add(VoidKind::Doc).unwrap();
5244 b.set_children(doc, &[list]).unwrap();
5245
5246 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5247 assert!(md.contains("1. one"), "{md}");
5248 assert!(md.contains("2. two"), "{md}");
5249 }
5250
5251 #[test]
5252 fn builder_rejects_invalid_kind_and_id() {
5253 let b = Builder::new().expect("builder");
5254 let mut id = 0u32;
5258 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
5259 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5260
5261 let mut ptr = std::ptr::null();
5263 let mut len = 0usize;
5264 let status =
5265 unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
5266 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5267 }
5268
5269 fn all_gestures() -> Vec<Gesture> {
5273 let inline = [
5274 InlineKind::Strong,
5275 InlineKind::Emph,
5276 InlineKind::Verbatim,
5277 InlineKind::Mark,
5278 InlineKind::Superscript,
5279 InlineKind::Subscript,
5280 InlineKind::Insert,
5281 InlineKind::Delete,
5282 ];
5283 let mut all: Vec<Gesture> = Vec::new();
5284 for k in inline {
5285 all.push(Gesture::WrapRange(k));
5286 all.push(Gesture::ToggleInline(k));
5287 }
5288 for k in [
5289 BlockContainerKind::BlockQuote,
5290 BlockContainerKind::BulletList,
5291 BlockContainerKind::OrderedList,
5292 ] {
5293 all.push(Gesture::ToggleBlockContainer(k));
5294 }
5295 all.extend([
5296 Gesture::SetBlock,
5297 Gesture::InsertThematicBreak,
5298 Gesture::ToggleCodeBlock,
5299 Gesture::SetCodeLanguage,
5300 Gesture::ToggleTaskItem,
5301 Gesture::SetTaskChecked,
5302 Gesture::ToggleTaskChecked,
5303 Gesture::InsertLink,
5304 Gesture::InsertImage,
5305 Gesture::InsertFootnote,
5306 Gesture::InsertLiteral,
5307 Gesture::InsertLineBreak,
5308 ]);
5309 all
5310 }
5311
5312 #[test]
5313 fn supports_answers_per_gesture_where_authorable_cannot() {
5314 assert!(Format::Html.is_authorable());
5319 assert!(Format::Html.supports(Gesture::ToggleInline(InlineKind::Strong)));
5320 assert!(!Format::Html.supports(Gesture::SetBlock));
5321 assert!(!Format::Html.supports(Gesture::ToggleBlockContainer(
5322 BlockContainerKind::BlockQuote
5323 )));
5324 assert!(!Format::Html.supports(Gesture::ToggleCodeBlock));
5325 assert!(!Format::Html.supports(Gesture::InsertLiteral));
5326
5327 for fmt in [Format::Xml, Format::Asciidoc] {
5330 assert!(!fmt.is_authorable());
5331 for g in all_gestures() {
5332 assert!(!fmt.supports(g), "{fmt:?} claims to spell {g:?}");
5333 }
5334 }
5335
5336 assert!(Format::Djot.supports(Gesture::ToggleInline(InlineKind::Mark)));
5339 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
5340 assert!(Format::Markdown.supports(Gesture::InsertLineBreak));
5341 assert!(!Format::Djot.supports(Gesture::InsertLineBreak));
5342 }
5343
5344 #[test]
5345 fn supports_agrees_with_what_the_editor_then_does() {
5346 for fmt in [Format::Djot, Format::Markdown, Format::Html] {
5351 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5352 let claimed = fmt.supports(Gesture::ToggleInline(InlineKind::Mark));
5353 let observed = ed.toggle_inline(0, 2, InlineKind::Mark);
5354 assert_eq!(
5355 claimed,
5356 !matches!(observed, Err(Error::UnsupportedFormat)),
5357 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5358 );
5359
5360 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5361 let claimed = fmt.supports(Gesture::SetBlock);
5362 let observed = ed.set_block(0, BlockKind::Heading(1));
5363 assert_eq!(
5364 claimed,
5365 !matches!(observed, Err(Error::UnsupportedFormat)),
5366 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5367 );
5368 }
5369 }
5370
5371 #[test]
5372 fn supports_rides_the_gestures_own_kind_space() {
5373 let (g, k) = Gesture::ToggleBlockContainer(BlockContainerKind::BulletList).to_c();
5378 assert_eq!((g, k), (3, 1));
5379 let (g, k) = Gesture::ToggleInline(InlineKind::Emph).to_c();
5380 assert_eq!((g, k), (1, 1));
5381 assert_eq!(Gesture::InsertLink.to_c(), (10, 0));
5384
5385 let mut out: c_int = 0;
5387 let status = unsafe {
5388 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 10, 3, &mut out)
5389 };
5390 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5391 let status = unsafe {
5392 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 9999, 0, &mut out)
5393 };
5394 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5395 }
5396}