1mod error;
2
3pub(crate) use twig_sys as ffi;
8
9use std::marker::PhantomData;
10use std::ops::Range;
11use std::os::raw::{c_char, c_int};
12use std::ptr::NonNull;
13
14pub use error::Error;
15pub use ffi::TwigSpan as Span;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25#[non_exhaustive]
26pub enum Format {
27 Djot,
28 Markdown,
29 Xml,
30 Html,
31 Asciidoc,
42}
43
44impl From<Format> for ffi::TwigFormat {
45 fn from(value: Format) -> Self {
46 match value {
47 Format::Djot => ffi::TwigFormat::Djot,
48 Format::Markdown => ffi::TwigFormat::Markdown,
49 Format::Xml => ffi::TwigFormat::Xml,
50 Format::Html => ffi::TwigFormat::Html,
51 Format::Asciidoc => ffi::TwigFormat::Asciidoc,
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71#[non_exhaustive]
72pub enum Target {
73 Djot,
74 Markdown,
75 Xml,
76 Html,
77 Asciidoc,
81}
82
83impl Target {
84 pub fn as_format(self) -> Option<Format> {
91 match self {
92 Target::Djot => Some(Format::Djot),
93 Target::Markdown => Some(Format::Markdown),
94 Target::Xml => Some(Format::Xml),
95 Target::Html => Some(Format::Html),
96 Target::Asciidoc => Some(Format::Asciidoc),
97 }
98 }
99}
100
101impl From<Format> for Target {
105 fn from(value: Format) -> Self {
106 match value {
107 Format::Djot => Target::Djot,
108 Format::Markdown => Target::Markdown,
109 Format::Xml => Target::Xml,
110 Format::Html => Target::Html,
111 Format::Asciidoc => Target::Asciidoc,
112 }
113 }
114}
115
116impl From<Target> for ffi::TwigFormat {
117 fn from(value: Target) -> Self {
118 match value {
119 Target::Djot => ffi::TwigFormat::Djot,
120 Target::Markdown => ffi::TwigFormat::Markdown,
121 Target::Xml => ffi::TwigFormat::Xml,
122 Target::Html => ffi::TwigFormat::Html,
123 Target::Asciidoc => ffi::TwigFormat::Asciidoc,
124 }
125 }
126}
127
128#[derive(Clone, Debug, Eq, PartialEq, Hash)]
162#[non_exhaustive]
163pub enum Kind {
164 Doc,
166 Para,
168 Heading,
169 ThematicBreak,
170 Section,
171 CodeBlock,
172 RawBlock,
173 Metadata,
174 BlockQuote,
175 BulletList,
176 OrderedList,
177 TaskList,
178 DefinitionList,
179 LineBlock,
180 Table,
181 ListItem,
183 TaskListItem,
184 DefinitionListItem,
185 Term,
186 Definition,
187 Line,
188 Row,
189 Cell,
190 Column,
191 Caption,
192 Footnote,
193 Reference,
194 Citation,
195 Substitution,
196 Str,
198 SoftBreak,
199 HardBreak,
200 NonBreakingSpace,
201 RawInline,
202 SmartPunctuation,
203 Link,
204 Image,
205 Emph,
207 Strong,
208 Mark,
209 Superscript,
210 Subscript,
211 Insert,
212 Delete,
213 DoubleQuoted,
214 SingleQuoted,
215 Symb,
217 Verbatim,
218 InlineMath,
219 DisplayMath,
220 Url,
221 Email,
222 FootnoteReference,
223 CitationReference,
224 SubstitutionReference,
225 Container,
227 ProcessingInstruction,
228 Comment,
229 Doctype,
230 Cdata,
231 Other(String),
238}
239
240impl Kind {
241 pub fn as_str(&self) -> &str {
244 match self {
245 Kind::Doc => "doc",
246 Kind::Para => "para",
247 Kind::Heading => "heading",
248 Kind::ThematicBreak => "thematic_break",
249 Kind::Section => "section",
250 Kind::CodeBlock => "code_block",
251 Kind::RawBlock => "raw_block",
252 Kind::Metadata => "metadata",
253 Kind::BlockQuote => "block_quote",
254 Kind::BulletList => "bullet_list",
255 Kind::OrderedList => "ordered_list",
256 Kind::TaskList => "task_list",
257 Kind::DefinitionList => "definition_list",
258 Kind::LineBlock => "line_block",
259 Kind::Table => "table",
260 Kind::ListItem => "list_item",
261 Kind::TaskListItem => "task_list_item",
262 Kind::DefinitionListItem => "definition_list_item",
263 Kind::Term => "term",
264 Kind::Definition => "definition",
265 Kind::Line => "line",
266 Kind::Row => "row",
267 Kind::Cell => "cell",
268 Kind::Column => "column",
269 Kind::Caption => "caption",
270 Kind::Footnote => "footnote",
271 Kind::Reference => "reference",
272 Kind::Citation => "citation",
273 Kind::Substitution => "substitution",
274 Kind::Str => "str",
275 Kind::SoftBreak => "soft_break",
276 Kind::HardBreak => "hard_break",
277 Kind::NonBreakingSpace => "non_breaking_space",
278 Kind::RawInline => "raw_inline",
279 Kind::SmartPunctuation => "smart_punctuation",
280 Kind::Link => "link",
281 Kind::Image => "image",
282 Kind::Container => "container",
283 Kind::ProcessingInstruction => "processing_instruction",
284 Kind::Emph => "emph",
285 Kind::Strong => "strong",
286 Kind::Mark => "mark",
287 Kind::Superscript => "superscript",
288 Kind::Subscript => "subscript",
289 Kind::Insert => "insert",
290 Kind::Delete => "delete",
291 Kind::DoubleQuoted => "double_quoted",
292 Kind::SingleQuoted => "single_quoted",
293 Kind::Symb => "symb",
294 Kind::Verbatim => "verbatim",
295 Kind::InlineMath => "inline_math",
296 Kind::DisplayMath => "display_math",
297 Kind::Url => "url",
298 Kind::Email => "email",
299 Kind::FootnoteReference => "footnote_reference",
300 Kind::CitationReference => "citation_reference",
301 Kind::SubstitutionReference => "substitution_reference",
302 Kind::Comment => "comment",
303 Kind::Doctype => "doctype",
304 Kind::Cdata => "cdata",
305 Kind::Other(name) => name.as_str(),
306 }
307 }
308
309 pub fn is_unknown(&self) -> bool {
313 matches!(self, Kind::Other(_))
314 }
315}
316
317impl From<&str> for Kind {
318 fn from(name: &str) -> Self {
319 match name {
320 "doc" => Kind::Doc,
321 "para" => Kind::Para,
322 "heading" => Kind::Heading,
323 "thematic_break" => Kind::ThematicBreak,
324 "section" => Kind::Section,
325 "code_block" => Kind::CodeBlock,
326 "raw_block" => Kind::RawBlock,
327 "metadata" => Kind::Metadata,
328 "block_quote" => Kind::BlockQuote,
329 "bullet_list" => Kind::BulletList,
330 "ordered_list" => Kind::OrderedList,
331 "task_list" => Kind::TaskList,
332 "definition_list" => Kind::DefinitionList,
333 "line_block" => Kind::LineBlock,
334 "table" => Kind::Table,
335 "list_item" => Kind::ListItem,
336 "task_list_item" => Kind::TaskListItem,
337 "definition_list_item" => Kind::DefinitionListItem,
338 "term" => Kind::Term,
339 "definition" => Kind::Definition,
340 "line" => Kind::Line,
341 "row" => Kind::Row,
342 "cell" => Kind::Cell,
343 "column" => Kind::Column,
344 "caption" => Kind::Caption,
345 "footnote" => Kind::Footnote,
346 "reference" => Kind::Reference,
347 "citation" => Kind::Citation,
348 "substitution" => Kind::Substitution,
349 "str" => Kind::Str,
350 "soft_break" => Kind::SoftBreak,
351 "hard_break" => Kind::HardBreak,
352 "non_breaking_space" => Kind::NonBreakingSpace,
353 "raw_inline" => Kind::RawInline,
354 "smart_punctuation" => Kind::SmartPunctuation,
355 "link" => Kind::Link,
356 "image" => Kind::Image,
357 "container" => Kind::Container,
358 "processing_instruction" => Kind::ProcessingInstruction,
359 "emph" => Kind::Emph,
360 "strong" => Kind::Strong,
361 "mark" => Kind::Mark,
362 "superscript" => Kind::Superscript,
363 "subscript" => Kind::Subscript,
364 "insert" => Kind::Insert,
365 "delete" => Kind::Delete,
366 "double_quoted" => Kind::DoubleQuoted,
367 "single_quoted" => Kind::SingleQuoted,
368 "symb" => Kind::Symb,
369 "verbatim" => Kind::Verbatim,
370 "inline_math" => Kind::InlineMath,
371 "display_math" => Kind::DisplayMath,
372 "url" => Kind::Url,
373 "email" => Kind::Email,
374 "footnote_reference" => Kind::FootnoteReference,
375 "citation_reference" => Kind::CitationReference,
376 "substitution_reference" => Kind::SubstitutionReference,
377 "comment" => Kind::Comment,
378 "doctype" => Kind::Doctype,
379 "cdata" => Kind::Cdata,
380 other => Kind::Other(other.to_string()),
381 }
382 }
383}
384
385impl std::fmt::Display for Kind {
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 f.write_str(self.as_str())
388 }
389}
390
391#[derive(Clone, Debug, Eq, PartialEq)]
393pub struct QueryMatch {
394 pub node_id: u32,
396 pub span: Range<usize>,
398 pub content_span: Option<Range<usize>>,
401 pub kind: Kind,
404}
405
406#[derive(Clone, Debug, Eq, PartialEq)]
413pub struct Change {
414 pub old: Range<usize>,
415 pub new: Range<usize>,
416}
417
418impl Change {
419 pub fn delta(&self) -> isize {
421 self.new.len() as isize - self.old.len() as isize
422 }
423
424 fn from_ffi(c: ffi::TwigChange) -> Self {
425 Change {
426 old: c.old_span.start..c.old_span.end,
427 new: c.new_span.start..c.new_span.end,
428 }
429 }
430}
431
432#[derive(Clone, Debug, Eq, PartialEq)]
443#[non_exhaustive]
444pub struct FlatNode {
445 pub id: NodeId,
446 pub parent: Option<NodeId>,
447 pub first_child: Option<NodeId>,
448 pub next_sibling: Option<NodeId>,
449 pub span: Range<usize>,
450 pub content_span: Option<Range<usize>>,
451 pub level: Option<u32>,
453 pub kind: Kind,
454 pub text: Option<String>,
455 pub destination: Option<String>,
456 pub head: Option<bool>,
459 pub alignment: Option<Alignment>,
465 pub name: Option<String>,
477 pub directive_form: Option<DirectiveForm>,
492 pub origin: Option<ContainerOrigin>,
502 pub marker_span: Option<Range<usize>>,
521 pub checked: Option<bool>,
534 pub attrs: Vec<(String, Option<String>)>,
538}
539
540#[derive(Clone, Debug, Default, Eq, PartialEq)]
548pub struct LinePrefix {
549 pub text: String,
551 pub columns: usize,
553}
554
555#[derive(Clone, Copy, Debug, Eq, PartialEq)]
560pub enum InlineKind {
561 Strong,
562 Emph,
563 Verbatim,
564 Mark,
565 Superscript,
566 Subscript,
567 Insert,
568 Delete,
569}
570
571impl InlineKind {
572 fn to_c(self) -> c_int {
573 match self {
574 InlineKind::Strong => 0,
575 InlineKind::Emph => 1,
576 InlineKind::Verbatim => 2,
577 InlineKind::Mark => 3,
578 InlineKind::Superscript => 4,
579 InlineKind::Subscript => 5,
580 InlineKind::Insert => 6,
581 InlineKind::Delete => 7,
582 }
583 }
584}
585
586#[derive(Clone, Copy, Debug, Eq, PartialEq)]
588pub enum BlockKind {
589 Paragraph,
590 Heading(u32),
592}
593
594impl BlockKind {
595 fn to_c(self) -> (c_int, u32) {
597 match self {
598 BlockKind::Paragraph => (0, 0),
599 BlockKind::Heading(level) => (1, level),
600 }
601 }
602}
603
604#[derive(Clone, Copy, Debug, Eq, PartialEq)]
610pub enum BlockContainerKind {
611 BlockQuote,
612 BulletList,
613 OrderedList,
614}
615
616impl BlockContainerKind {
617 fn to_c(self) -> c_int {
618 match self {
619 BlockContainerKind::BlockQuote => 0,
620 BlockContainerKind::BulletList => 1,
621 BlockContainerKind::OrderedList => 2,
622 }
623 }
624}
625
626#[derive(Clone, Copy, Debug, Eq, PartialEq)]
665#[non_exhaustive]
666pub enum Gesture {
667 WrapRange(InlineKind),
668 ToggleInline(InlineKind),
669 SetBlock,
670 ToggleBlockContainer(BlockContainerKind),
671 InsertThematicBreak,
672 ToggleCodeBlock,
673 SetCodeLanguage,
674 ToggleTaskItem,
675 SetTaskChecked,
676 ToggleTaskChecked,
677 InsertLink,
678 InsertImage,
679 InsertFootnote,
680 InsertLiteral,
681 InsertLineBreak,
682 SplitBlock,
683 RenumberOrderedLists,
684 TableInsertRow,
685 TableDeleteRow,
686 TableInsertColumn,
687 TableDeleteColumn,
688 TableSetAlignment,
689 TableMoveRow,
690 TableMoveColumn,
691}
692
693impl Gesture {
694 fn to_c(self) -> (c_int, c_int) {
699 match self {
700 Gesture::WrapRange(k) => (0, k.to_c()),
701 Gesture::ToggleInline(k) => (1, k.to_c()),
702 Gesture::SetBlock => (2, 0),
703 Gesture::ToggleBlockContainer(k) => (3, k.to_c()),
704 Gesture::InsertThematicBreak => (4, 0),
705 Gesture::ToggleCodeBlock => (5, 0),
706 Gesture::SetCodeLanguage => (6, 0),
707 Gesture::ToggleTaskItem => (7, 0),
708 Gesture::SetTaskChecked => (8, 0),
709 Gesture::ToggleTaskChecked => (9, 0),
710 Gesture::InsertLink => (10, 0),
711 Gesture::InsertImage => (11, 0),
712 Gesture::InsertFootnote => (12, 0),
713 Gesture::InsertLiteral => (13, 0),
714 Gesture::InsertLineBreak => (14, 0),
715 Gesture::SplitBlock => (15, 0),
716 Gesture::RenumberOrderedLists => (16, 0),
717 Gesture::TableInsertRow => (17, 0),
718 Gesture::TableDeleteRow => (18, 0),
719 Gesture::TableInsertColumn => (19, 0),
720 Gesture::TableDeleteColumn => (20, 0),
721 Gesture::TableSetAlignment => (21, 0),
722 Gesture::TableMoveRow => (22, 0),
723 Gesture::TableMoveColumn => (23, 0),
724 }
725 }
726}
727
728impl Format {
729 pub fn supports(self, gesture: Gesture) -> bool {
754 let (g, k) = gesture.to_c();
755 let mut supported: c_int = 0;
756 let status = unsafe {
757 ffi::twig_format_supports(ffi::TwigFormat::from(self) as c_int, g, k, &mut supported)
758 };
759 debug_assert!(
760 Error::from_status(status).is_ok(),
761 "twig_format_supports rejected a combination the Rust types make unrepresentable",
762 );
763 supported == 1
764 }
765
766 pub fn is_authorable(self) -> bool {
777 let mut authorable: c_int = 0;
778 let status = unsafe {
779 ffi::twig_format_is_authorable(ffi::TwigFormat::from(self) as c_int, &mut authorable)
780 };
781 debug_assert!(Error::from_status(status).is_ok(), "unknown format code");
782 authorable == 1
783 }
784}
785
786#[derive(Clone, Copy, Debug, Eq, PartialEq)]
787pub struct Version {
788 pub major: u8,
789 pub minor: u8,
790 pub patch: u8,
791}
792
793pub fn version() -> Version {
794 let packed = unsafe { ffi::twig_version() };
795 Version {
796 major: (packed >> 16) as u8,
797 minor: (packed >> 8) as u8,
798 patch: packed as u8,
799 }
800}
801
802pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
808
809pub fn abi_version() -> u32 {
815 unsafe { ffi::twig_abi_version() }
816}
817
818pub fn version_string() -> &'static str {
819 let ptr = unsafe { ffi::twig_version_string() };
820 unsafe { std::ffi::CStr::from_ptr(ptr) }
821 .to_str()
822 .unwrap_or("")
823}
824
825#[derive(Debug)]
826pub struct Document {
827 raw: NonNull<ffi::TwigDocument>,
828}
829
830impl Document {
831 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
832 Self::parse_with(input, format, MarkdownExtensions::default())
833 }
834
835 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
836 Self::parse(input.as_bytes(), format)
837 }
838
839 pub fn parse_with(
845 input: &[u8],
846 format: Format,
847 extensions: MarkdownExtensions,
848 ) -> Result<Self, Error> {
849 let mut raw = std::ptr::null_mut();
850 let ffi_format: ffi::TwigFormat = format.into();
851 let status = unsafe {
852 ffi::twig_parse_ext(
853 input.as_ptr(),
854 input.len(),
855 ffi_format as i32,
856 extensions.to_flags(),
857 &mut raw,
858 )
859 };
860 Error::from_status(status)?;
861 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
862 Ok(Self { raw })
863 }
864
865 pub fn parse_str_with(
867 input: &str,
868 format: Format,
869 extensions: MarkdownExtensions,
870 ) -> Result<Self, Error> {
871 Self::parse_with(input.as_bytes(), format, extensions)
872 }
873
874 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
877 let raw = self.raw.as_ptr();
878 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
879 }
880
881 pub fn serialize_to(&mut self, target: Target) -> Result<Vec<u8>, Error> {
892 let raw = self.raw.as_ptr();
893 let ffi_target: ffi::TwigFormat = target.into();
894 collect_bytes(|ptr, len| unsafe {
895 ffi::twig_document_serialize(raw, ffi_target as i32, ptr, len)
896 })
897 }
898
899 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
906 self.serialize_to(format.into())
907 }
908
909 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
912 let raw = self.raw.as_ptr();
913 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
914 }
915
916 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
925 let raw = self.raw.as_ptr();
926 collect_matches(|ptr, len| unsafe {
927 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
928 })
929 }
930
931 pub fn span(&mut self, node: NodeId) -> Result<Range<usize>, Error> {
933 let mut span = ffi::TwigSpan { start: 0, end: 0 };
934 let status = unsafe { ffi::twig_document_node_span(self.raw.as_ptr(), node.0, &mut span) };
935 Error::from_status(status)?;
936 Ok(span.start..span.end)
937 }
938
939 pub fn content_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
942 let mut span = ffi::TwigSpan { start: 0, end: 0 };
943 let status =
944 unsafe { ffi::twig_document_node_content_span(self.raw.as_ptr(), node.0, &mut span) };
945 match status.0 {
946 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
947 ffi::TwigStatus::NOT_FOUND => Ok(None),
948 _ => Err(Error::from_status(status).unwrap_err()),
949 }
950 }
951
952 pub fn marker_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
956 let mut span = ffi::TwigSpan { start: 0, end: 0 };
957 let status =
958 unsafe { ffi::twig_document_node_marker_span(self.raw.as_ptr(), node.0, &mut span) };
959 match status.0 {
960 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
961 ffi::TwigStatus::NOT_FOUND => Ok(None),
962 _ => Err(Error::from_status(status).unwrap_err()),
963 }
964 }
965
966 pub fn attrs_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
981 let mut span = ffi::TwigSpan { start: 0, end: 0 };
982 let status =
983 unsafe { ffi::twig_document_attrs_span(self.raw.as_ptr(), node.0, &mut span) };
984 match status.0 {
985 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
986 ffi::TwigStatus::NOT_FOUND => Ok(None),
987 _ => Err(Error::from_status(status).unwrap_err()),
988 }
989 }
990
991 pub fn line_prefix(&mut self, offset: usize) -> Result<Option<Range<usize>>, Error> {
1008 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1009 let status =
1010 unsafe { ffi::twig_document_line_prefix(self.raw.as_ptr(), offset, &mut span) };
1011 match status.0 {
1012 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1013 ffi::TwigStatus::NOT_FOUND => Ok(None),
1014 _ => Err(Error::from_status(status).unwrap_err()),
1015 }
1016 }
1017
1018 pub fn continuation_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1044 self.prefix_via(offset, ffi::twig_document_continuation_prefix)
1045 }
1046
1047 pub fn blank_line_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1059 self.prefix_via(offset, ffi::twig_document_blank_line_prefix)
1060 }
1061
1062 fn prefix_via(
1064 &mut self,
1065 offset: usize,
1066 f: unsafe extern "C" fn(
1067 *mut ffi::TwigDocument,
1068 usize,
1069 *mut *const u8,
1070 *mut usize,
1071 *mut usize,
1072 ) -> ffi::TwigStatus,
1073 ) -> Result<LinePrefix, Error> {
1074 let mut ptr: *const u8 = std::ptr::null();
1075 let mut len = 0usize;
1076 let mut columns = 0usize;
1077 let status = unsafe { f(self.raw.as_ptr(), offset, &mut ptr, &mut len, &mut columns) };
1078 Error::from_status(status)?;
1079 let text = if ptr.is_null() || len == 0 {
1080 String::new()
1081 } else {
1082 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1083 String::from_utf8(bytes.to_vec()).map_err(|_| Error::Internal)?
1084 };
1085 Ok(LinePrefix { text, columns })
1086 }
1087
1088 pub fn cell_extent(&mut self, node: NodeId) -> Result<Option<(u32, u32)>, Error> {
1100 let raw = self.raw.as_ptr();
1101 let mut colspan: u32 = 0;
1102 let status = unsafe { ffi::twig_document_cell_colspan(raw, node.0, &mut colspan) };
1103 match status.0 {
1104 ffi::TwigStatus::OK => {}
1105 ffi::TwigStatus::NOT_FOUND => return Ok(None),
1106 _ => return Err(Error::from_status(status).unwrap_err()),
1107 }
1108 let mut rowspan: u32 = 0;
1109 Error::from_status(unsafe { ffi::twig_document_cell_rowspan(raw, node.0, &mut rowspan) })?;
1110 Ok(Some((colspan, rowspan)))
1111 }
1112
1113 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1118 let raw = self.raw.as_ptr();
1119 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_nodes(raw, ptr, len) })
1120 }
1121
1122 pub fn definitions(&mut self) -> Result<Vec<QueryMatch>, Error> {
1139 let raw = self.raw.as_ptr();
1140 collect_matches(|ptr, len| unsafe { ffi::twig_document_definitions(raw, ptr, len) })
1141 }
1142
1143 pub fn diagnostics(&mut self, target: Target) -> Result<Vec<Warning>, Error> {
1163 let raw = self.raw.as_ptr();
1164 let code = ffi::TwigFormat::from(target) as c_int;
1165 let mut ptr: *const ffi::TwigWarning = std::ptr::null();
1166 let mut len = 0usize;
1167 let status = unsafe { ffi::twig_document_diagnostics(raw, code, &mut ptr, &mut len) };
1168 Error::from_status(status)?;
1169 if len == 0 || ptr.is_null() {
1170 return Ok(Vec::new());
1171 }
1172 let raw_warnings = unsafe { std::slice::from_raw_parts(ptr, len) };
1173 Ok(raw_warnings
1174 .iter()
1175 .map(|w| Warning {
1176 fidelity: Fidelity::from_c(w.fidelity),
1177 path: borrowed_bytes(w.path_ptr, w.path_len).unwrap_or_default(),
1178 kind: Kind::from(borrowed_cstr(w.kind).unwrap_or_default().as_str()),
1179 })
1180 .collect())
1181 }
1182
1183 pub fn children(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1189 let raw = self.raw.as_ptr();
1190 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1191 collect_matches(|ptr, len| unsafe { ffi::twig_document_children(raw, id, ptr, len) })
1192 }
1193
1194 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1200 let raw = self.raw.as_ptr();
1201 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_subtree(raw, node.0, ptr, len) })
1202 }
1203
1204 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1209 let mut m = empty_ffi_match();
1210 let status = unsafe { ffi::twig_document_node_at(self.raw.as_ptr(), offset, &mut m) };
1211 match status.0 {
1212 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1213 ffi::TwigStatus::NOT_FOUND => Ok(None),
1214 _ => Err(Error::from_status(status).unwrap_err()),
1215 }
1216 }
1217
1218 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1222 let raw = self.raw.as_ptr();
1223 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1224 let mut len = 0usize;
1225 let status = unsafe { ffi::twig_document_nodes_at(raw, offset, &mut ptr, &mut len) };
1226 match status.0 {
1227 ffi::TwigStatus::OK => {}
1228 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1229 _ => return Err(Error::from_status(status).unwrap_err()),
1230 }
1231 if len == 0 || ptr.is_null() {
1232 return Ok(Vec::new());
1233 }
1234 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1235 raw_matches.iter().map(query_match_from_ffi).collect()
1236 }
1237
1238 pub fn node_at_caret(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1261 let mut m = empty_ffi_match();
1262 let status = unsafe { ffi::twig_document_node_at_caret(self.raw.as_ptr(), offset, &mut m) };
1263 match status.0 {
1264 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1265 ffi::TwigStatus::NOT_FOUND => Ok(None),
1266 _ => Err(Error::from_status(status).unwrap_err()),
1267 }
1268 }
1269
1270 pub fn ancestors_at_caret(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1274 let raw = self.raw.as_ptr();
1275 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1276 let mut len = 0usize;
1277 let status = unsafe { ffi::twig_document_nodes_at_caret(raw, offset, &mut ptr, &mut len) };
1278 match status.0 {
1279 ffi::TwigStatus::OK => {}
1280 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1281 _ => return Err(Error::from_status(status).unwrap_err()),
1282 }
1283 if len == 0 || ptr.is_null() {
1284 return Ok(Vec::new());
1285 }
1286 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1287 raw_matches.iter().map(query_match_from_ffi).collect()
1288 }
1289}
1290
1291#[derive(Debug)]
1302pub struct DocumentView<'a> {
1303 doc: Document,
1304 _editor: PhantomData<&'a mut Editor>,
1305}
1306
1307impl std::ops::Deref for DocumentView<'_> {
1308 type Target = Document;
1309
1310 fn deref(&self) -> &Document {
1311 &self.doc
1312 }
1313}
1314
1315impl std::ops::DerefMut for DocumentView<'_> {
1316 fn deref_mut(&mut self) -> &mut Document {
1317 &mut self.doc
1318 }
1319}
1320
1321impl Drop for Document {
1322 fn drop(&mut self) {
1323 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
1324 }
1325}
1326
1327#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1333pub struct MarkdownExtensions {
1334 pub directives: bool,
1336 pub math: bool,
1338 pub html_elements: bool,
1343 pub highlight: bool,
1346 pub highlight_colors: bool,
1352}
1353
1354impl MarkdownExtensions {
1355 fn to_flags(self) -> u32 {
1356 let mut flags = 0;
1357 if self.directives {
1358 flags |= ffi::TWIG_MD_DIRECTIVES;
1359 }
1360 if self.math {
1361 flags |= ffi::TWIG_MD_MATH;
1362 }
1363 if self.html_elements {
1364 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
1365 }
1366 if self.highlight {
1367 flags |= ffi::TWIG_MD_HIGHLIGHT;
1368 }
1369 if self.highlight_colors {
1370 flags |= ffi::TWIG_MD_HIGHLIGHT_COLORS;
1371 }
1372 flags
1373 }
1374}
1375
1376#[derive(Debug)]
1382pub struct Editor {
1383 raw: NonNull<ffi::TwigEditor>,
1384}
1385
1386impl Editor {
1387 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
1390 let mut raw = std::ptr::null_mut();
1391 let ffi_format: ffi::TwigFormat = format.into();
1392 let status = unsafe {
1393 ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw)
1394 };
1395 Error::from_status(status)?;
1396 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1397 Ok(Self { raw })
1398 }
1399
1400 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
1401 Self::new(input.as_bytes(), format)
1402 }
1403
1404 pub fn new_ext(
1409 input: &[u8],
1410 format: Format,
1411 extensions: MarkdownExtensions,
1412 ) -> Result<Self, Error> {
1413 let mut raw = std::ptr::null_mut();
1414 let ffi_format: ffi::TwigFormat = format.into();
1415 let status = unsafe {
1416 ffi::twig_editor_create_ext(
1417 input.as_ptr(),
1418 input.len(),
1419 ffi_format as i32,
1420 extensions.to_flags(),
1421 &mut raw,
1422 )
1423 };
1424 Error::from_status(status)?;
1425 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1426 Ok(Self { raw })
1427 }
1428
1429 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1431 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1432 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
1433 })
1434 }
1435
1436 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1439 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1440 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
1441 })
1442 }
1443
1444 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1446 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1447 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
1448 })
1449 }
1450
1451 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1453 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1454 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
1455 })
1456 }
1457
1458 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
1461 let status = unsafe {
1462 ffi::twig_editor_insert_child(
1463 self.raw.as_ptr(),
1464 locator.as_ptr(),
1465 locator.len(),
1466 index,
1467 text.as_ptr(),
1468 text.len(),
1469 )
1470 };
1471 Error::from_status(status)
1472 }
1473
1474 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
1477 let status =
1478 unsafe { ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1479 Error::from_status(status)
1480 }
1481
1482 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
1485 let status = unsafe {
1486 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
1487 };
1488 Error::from_status(status)
1489 }
1490
1491 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
1495 let status =
1496 unsafe { ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1497 Error::from_status(status)
1498 }
1499
1500 pub fn filter(
1505 &mut self,
1506 drop: &str,
1507 keep: Option<&str>,
1508 unwrap_kept: bool,
1509 ) -> Result<(), Error> {
1510 let (keep_ptr, keep_len) = match keep {
1511 Some(k) => (k.as_ptr(), k.len()),
1512 None => (std::ptr::null(), 0),
1513 };
1514 let status = unsafe {
1515 ffi::twig_editor_filter(
1516 self.raw.as_ptr(),
1517 drop.as_ptr(),
1518 drop.len(),
1519 keep_ptr,
1520 keep_len,
1521 unwrap_kept as i32,
1522 )
1523 };
1524 Error::from_status(status)
1525 }
1526
1527 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
1529 let raw = self.raw.as_ptr();
1530 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
1531 }
1532
1533 pub fn source_str(&mut self) -> Result<String, Error> {
1535 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
1536 }
1537
1538 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
1541 let raw = self.raw.as_ptr();
1542 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
1543 }
1544
1545 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1548 let raw = self.raw.as_ptr();
1549 collect_matches(|ptr, len| unsafe {
1550 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
1551 })
1552 }
1553
1554 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
1564 let mut change = ffi::TwigChange {
1565 old_span: ffi::TwigSpan { start: 0, end: 0 },
1566 new_span: ffi::TwigSpan { start: 0, end: 0 },
1567 };
1568 let status = unsafe {
1569 ffi::twig_editor_edit_range(
1570 self.raw.as_ptr(),
1571 start,
1572 end,
1573 text.as_ptr(),
1574 text.len(),
1575 &mut change,
1576 )
1577 };
1578 Error::from_status(status)?;
1579 Ok(Change::from_ffi(change))
1580 }
1581
1582 pub fn last_change(&mut self) -> Option<Change> {
1588 let mut change = ffi::TwigChange {
1589 old_span: ffi::TwigSpan { start: 0, end: 0 },
1590 new_span: ffi::TwigSpan { start: 0, end: 0 },
1591 };
1592 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
1593 match status.0 {
1594 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
1595 _ => None,
1596 }
1597 }
1598
1599 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
1604 let mut change = ffi::TwigChange {
1605 old_span: ffi::TwigSpan { start: 0, end: 0 },
1606 new_span: ffi::TwigSpan { start: 0, end: 0 },
1607 };
1608 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
1609 if status.0 == ffi::TwigStatus::NOT_FOUND {
1610 return Ok(None);
1611 }
1612 Error::from_status(status)?;
1613 Ok(Some(Change::from_ffi(change)))
1614 }
1615
1616 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
1620 let mut change = ffi::TwigChange {
1621 old_span: ffi::TwigSpan { start: 0, end: 0 },
1622 new_span: ffi::TwigSpan { start: 0, end: 0 },
1623 };
1624 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
1625 if status.0 == ffi::TwigStatus::NOT_FOUND {
1626 return Ok(None);
1627 }
1628 Error::from_status(status)?;
1629 Ok(Some(Change::from_ffi(change)))
1630 }
1631
1632 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
1637 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
1638 Error::from_status(status)
1639 }
1640
1641 pub fn revision(&mut self) -> u64 {
1647 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
1648 }
1649
1650 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
1671 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1672 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
1673 match status.0 {
1674 ffi::TwigStatus::OK => Some(span.start..span.end),
1675 _ => None,
1676 }
1677 }
1678
1679 pub fn clear_dirty(&mut self) {
1684 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
1685 }
1686
1687 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
1695 let status = unsafe {
1696 ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len())
1697 };
1698 Error::from_status(status)
1699 }
1700
1701 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
1706 let raw = self.raw.as_ptr();
1707 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
1708 }
1709
1710 pub fn document(&mut self) -> Result<DocumentView<'_>, Error> {
1719 let mut raw = std::ptr::null_mut();
1720 let status = unsafe { ffi::twig_editor_document(self.raw.as_ptr(), &mut raw) };
1721 Error::from_status(status)?;
1722 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1723 Ok(DocumentView {
1724 doc: Document { raw },
1725 _editor: PhantomData,
1726 })
1727 }
1728
1729 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1734 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1735 let mut len = 0usize;
1736 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
1737 Error::from_status(status)?;
1738 if len == 0 {
1739 return Ok(Vec::new());
1740 }
1741 if ptr.is_null() {
1742 return Err(Error::Internal);
1743 }
1744 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1745 raw.iter().map(flat_node_from_ffi).collect()
1746 }
1747
1748 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1755 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1756 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1757 let mut len = 0usize;
1758 let status =
1759 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
1760 Error::from_status(status)?;
1761 if len == 0 || ptr.is_null() {
1762 return Ok(Vec::new());
1763 }
1764 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1765 raw.iter().map(query_match_from_ffi).collect()
1766 }
1767
1768 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1776 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1777 let mut len = 0usize;
1778 let status =
1779 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
1780 Error::from_status(status)?;
1781 if len == 0 || ptr.is_null() {
1782 return Ok(Vec::new());
1783 }
1784 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1785 raw.iter().map(flat_node_from_ffi).collect()
1786 }
1787
1788 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1793 let mut m = ffi::TwigQueryMatch {
1794 node_id: 0,
1795 span: ffi::TwigSpan { start: 0, end: 0 },
1796 content_span: ffi::TwigSpan { start: 0, end: 0 },
1797 has_content_span: 0,
1798 kind: std::ptr::null(),
1799 };
1800 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
1801 match status.0 {
1802 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1803 ffi::TwigStatus::NOT_FOUND => Ok(None),
1804 _ => Err(Error::from_status(status).unwrap_err()),
1805 }
1806 }
1807
1808 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1812 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1813 let mut len = 0usize;
1814 let status =
1815 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
1816 match status.0 {
1817 ffi::TwigStatus::OK => {}
1818 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1819 _ => return Err(Error::from_status(status).unwrap_err()),
1820 }
1821 if len == 0 || ptr.is_null() {
1822 return Ok(Vec::new());
1823 }
1824 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1825 raw.iter().map(query_match_from_ffi).collect()
1826 }
1827
1828 pub fn wrap_range(
1836 &mut self,
1837 start: usize,
1838 end: usize,
1839 kind: InlineKind,
1840 ) -> Result<Change, Error> {
1841 self.change_op(|ed, out| unsafe {
1842 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
1843 })
1844 }
1845
1846 pub fn toggle_inline(
1851 &mut self,
1852 start: usize,
1853 end: usize,
1854 kind: InlineKind,
1855 ) -> Result<Change, Error> {
1856 self.change_op(|ed, out| unsafe {
1857 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
1858 })
1859 }
1860
1861 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
1880 let (block_kind, level) = kind.to_c();
1881 self.change_op(|ed, out| unsafe {
1882 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
1883 })
1884 }
1885
1886 pub fn toggle_block_container(
1909 &mut self,
1910 start: usize,
1911 end: usize,
1912 kind: BlockContainerKind,
1913 ) -> Result<Change, Error> {
1914 self.change_op(|ed, out| unsafe {
1915 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
1916 })
1917 }
1918
1919 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
1938 self.change_op(|ed, out| unsafe {
1939 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
1940 })?;
1941 Ok(())
1942 }
1943
1944 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
1953 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
1954 }
1955
1956 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
1959 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
1960 }
1961
1962 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
1964 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
1965 }
1966
1967 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
1969 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
1970 }
1971
1972 pub fn table_set_alignment(
1974 &mut self,
1975 offset: usize,
1976 alignment: Alignment,
1977 ) -> Result<(), Error> {
1978 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
1979 }
1980
1981 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
1983 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
1984 }
1985
1986 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
1988 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
1989 }
1990
1991 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
1992 self.change_op(|ed, out| unsafe { ffi::twig_editor_table_edit(ed, offset, op, arg, out) })?;
1993 Ok(())
1994 }
1995
1996 pub fn insert_link(
2041 &mut self,
2042 start: usize,
2043 end: usize,
2044 destination: &str,
2045 ) -> Result<Change, Error> {
2046 self.change_op(|ed, out| unsafe {
2047 ffi::twig_editor_insert_link(
2048 ed,
2049 start,
2050 end,
2051 destination.as_ptr(),
2052 destination.len(),
2053 out,
2054 )
2055 })
2056 }
2057
2058 pub fn insert_image(
2079 &mut self,
2080 start: usize,
2081 end: usize,
2082 destination: &str,
2083 ) -> Result<Change, Error> {
2084 self.change_op(|ed, out| unsafe {
2085 ffi::twig_editor_insert_image(
2086 ed,
2087 start,
2088 end,
2089 destination.as_ptr(),
2090 destination.len(),
2091 out,
2092 )
2093 })
2094 }
2095
2096 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
2117 self.change_op(|ed, out| unsafe {
2118 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
2119 })
2120 }
2121
2122 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
2136 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
2137 }
2138
2139 pub fn insert_thematic_break(&mut self, offset: usize) -> Result<Change, Error> {
2158 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_thematic_break(ed, offset, out) })
2159 }
2160
2161 pub fn split_block(&mut self, offset: usize) -> Result<Change, Error> {
2209 self.change_op(|ed, out| unsafe { ffi::twig_editor_split_block(ed, offset, out) })
2210 }
2211
2212 pub fn toggle_code_block(
2244 &mut self,
2245 start: usize,
2246 end: usize,
2247 language: Option<&str>,
2248 ) -> Result<Change, Error> {
2249 let (ptr, len, has) = opt_str(language);
2250 self.change_op(|ed, out| unsafe {
2251 ffi::twig_editor_toggle_code_block(ed, start, end, ptr, len, has, out)
2252 })
2253 }
2254
2255 pub fn set_code_language(
2265 &mut self,
2266 offset: usize,
2267 language: Option<&str>,
2268 ) -> Result<Change, Error> {
2269 let (ptr, len, has) = opt_str(language);
2270 self.change_op(|ed, out| unsafe {
2271 ffi::twig_editor_set_code_language(ed, offset, ptr, len, has, out)
2272 })
2273 }
2274
2275 pub fn toggle_task_item(&mut self, offset: usize) -> Result<Change, Error> {
2286 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_item(ed, offset, out) })
2287 }
2288
2289 pub fn set_task_checked(&mut self, offset: usize, checked: bool) -> Result<(), Error> {
2303 self.change_op(|ed, out| unsafe {
2304 ffi::twig_editor_set_task_checked(ed, offset, checked as c_int, out)
2305 })?;
2306 Ok(())
2307 }
2308
2309 pub fn toggle_task_checked(&mut self, offset: usize) -> Result<Change, Error> {
2314 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_checked(ed, offset, out) })
2315 }
2316
2317 pub fn insert_footnote(&mut self, offset: usize, label: &str) -> Result<Change, Error> {
2336 self.change_op(|ed, out| unsafe {
2337 ffi::twig_editor_insert_footnote(ed, offset, label.as_ptr(), label.len(), out)
2338 })
2339 }
2340
2341 fn change_op(
2344 &mut self,
2345 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
2346 ) -> Result<Change, Error> {
2347 let mut change = ffi::TwigChange {
2348 old_span: ffi::TwigSpan { start: 0, end: 0 },
2349 new_span: ffi::TwigSpan { start: 0, end: 0 },
2350 };
2351 let status = op(self.raw.as_ptr(), &mut change);
2352 Error::from_status(status)?;
2353 Ok(Change::from_ffi(change))
2354 }
2355
2356 fn apply(
2358 &mut self,
2359 locator: &str,
2360 text: &str,
2361 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
2362 ) -> Result<(), Error> {
2363 let status = op(
2364 self.raw.as_ptr(),
2365 locator.as_ptr(),
2366 locator.len(),
2367 text.as_ptr(),
2368 text.len(),
2369 );
2370 Error::from_status(status)
2371 }
2372}
2373
2374impl Drop for Editor {
2375 fn drop(&mut self) {
2376 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
2377 }
2378}
2379
2380fn collect_bytes(
2385 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
2386) -> Result<Vec<u8>, Error> {
2387 let mut ptr = std::ptr::null();
2388 let mut len = 0usize;
2389 let status = call(&mut ptr, &mut len);
2390 Error::from_status(status)?;
2391 if len == 0 {
2392 return Ok(Vec::new());
2393 }
2394 if ptr.is_null() {
2395 return Err(Error::Internal);
2396 }
2397 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2398 Ok(bytes.to_vec())
2399}
2400
2401fn collect_matches(
2404 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
2405) -> Result<Vec<QueryMatch>, Error> {
2406 let mut ptr = std::ptr::null();
2407 let mut len = 0usize;
2408 let status = call(&mut ptr, &mut len);
2409 Error::from_status(status)?;
2410 if len == 0 {
2411 return Ok(Vec::new());
2412 }
2413 if ptr.is_null() {
2414 return Err(Error::Internal);
2415 }
2416 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
2417 matches.iter().map(query_match_from_ffi).collect()
2418}
2419
2420fn collect_flat_nodes(
2423 call: impl FnOnce(*mut *const ffi::TwigFlatNode, *mut usize) -> ffi::TwigStatus,
2424) -> Result<Vec<FlatNode>, Error> {
2425 let mut ptr = std::ptr::null();
2426 let mut len = 0usize;
2427 let status = call(&mut ptr, &mut len);
2428 Error::from_status(status)?;
2429 if len == 0 {
2430 return Ok(Vec::new());
2431 }
2432 if ptr.is_null() {
2433 return Err(Error::Internal);
2434 }
2435 let nodes = unsafe { std::slice::from_raw_parts(ptr, len) };
2436 nodes.iter().map(flat_node_from_ffi).collect()
2437}
2438
2439fn empty_ffi_match() -> ffi::TwigQueryMatch {
2441 ffi::TwigQueryMatch {
2442 node_id: 0,
2443 span: ffi::TwigSpan { start: 0, end: 0 },
2444 content_span: ffi::TwigSpan { start: 0, end: 0 },
2445 has_content_span: 0,
2446 kind: std::ptr::null(),
2447 }
2448}
2449
2450fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
2453 Ok(QueryMatch {
2454 node_id: m.node_id,
2455 span: m.span.start..m.span.end,
2456 content_span: if m.has_content_span != 0 {
2457 Some(m.content_span.start..m.content_span.end)
2458 } else {
2459 None
2460 },
2461 kind: Kind::from(borrowed_cstr(m.kind)?.as_str()),
2462 })
2463}
2464
2465fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
2467 let node_id = |v: u32| {
2468 if v == ffi::TWIG_NO_NODE {
2469 None
2470 } else {
2471 Some(NodeId(v))
2472 }
2473 };
2474 Ok(FlatNode {
2475 id: NodeId(n.id),
2476 parent: node_id(n.parent),
2477 first_child: node_id(n.first_child),
2478 next_sibling: node_id(n.next_sibling),
2479 span: n.span.start..n.span.end,
2480 content_span: if n.has_content_span != 0 {
2481 Some(n.content_span.start..n.content_span.end)
2482 } else {
2483 None
2484 },
2485 level: if n.level != 0 { Some(n.level) } else { None },
2486 kind: Kind::from(borrowed_cstr(n.kind)?.as_str()),
2487 text: borrowed_bytes(n.text_ptr, n.text_len),
2488 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
2489 head: match n.head {
2490 ffi::TWIG_HEAD_NONE => None,
2491 v => Some(v != 0),
2492 },
2493 alignment: Alignment::from_c(n.alignment),
2494 name: borrowed_bytes(n.name_ptr, n.name_len),
2495 directive_form: DirectiveForm::from_c(n.directive_form),
2496 origin: ContainerOrigin::from_c(n.container_origin),
2497 marker_span: if n.has_marker_span != 0 {
2498 Some(n.marker_span.start..n.marker_span.end)
2499 } else {
2500 None
2501 },
2502 checked: match n.checked {
2503 ffi::TWIG_TASK_CHECKED_NONE => None,
2504 v => Some(v != 0),
2505 },
2506 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
2507 })
2508}
2509
2510fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
2514 if ptr.is_null() || len == 0 {
2515 return Vec::new();
2516 }
2517 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
2518 kvs.iter()
2519 .map(|kv| {
2520 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
2521 (key, borrowed_bytes(kv.value, kv.value_len))
2522 })
2523 .collect()
2524}
2525
2526fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
2528 if ptr.is_null() {
2529 return Err(Error::Internal);
2530 }
2531 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
2532 .to_str()
2533 .map_err(|_| Error::Internal)?
2534 .to_owned())
2535}
2536
2537fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
2541 if ptr.is_null() {
2542 return None;
2543 }
2544 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2545 Some(String::from_utf8_lossy(bytes).into_owned())
2546}
2547
2548#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
2552pub struct NodeId(pub u32);
2553
2554#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2557pub enum VoidKind {
2558 Doc,
2559 Para,
2560 ThematicBreak,
2561 Section,
2562 Div,
2563 BlockQuote,
2564 DefinitionList,
2565 Table,
2566 ListItem,
2567 DefinitionListItem,
2568 Term,
2569 Definition,
2570 Caption,
2571 SoftBreak,
2572 HardBreak,
2573 NonBreakingSpace,
2574 Emph,
2575 Strong,
2576 Span,
2577 Mark,
2578 Superscript,
2579 Subscript,
2580 Insert,
2581 Delete,
2582 DoubleQuoted,
2583 SingleQuoted,
2584}
2585
2586impl VoidKind {
2587 fn to_c(self) -> c_int {
2588 match self {
2590 VoidKind::Doc => 0,
2591 VoidKind::Para => 1,
2592 VoidKind::ThematicBreak => 3,
2593 VoidKind::Section => 4,
2594 VoidKind::Div => 5,
2595 VoidKind::BlockQuote => 9,
2596 VoidKind::DefinitionList => 13,
2597 VoidKind::Table => 14,
2598 VoidKind::ListItem => 15,
2599 VoidKind::DefinitionListItem => 17,
2600 VoidKind::Term => 18,
2601 VoidKind::Definition => 19,
2602 VoidKind::Caption => 22,
2603 VoidKind::SoftBreak => 26,
2604 VoidKind::HardBreak => 27,
2605 VoidKind::NonBreakingSpace => 28,
2606 VoidKind::Emph => 38,
2607 VoidKind::Strong => 39,
2608 VoidKind::Span => 42,
2609 VoidKind::Mark => 43,
2610 VoidKind::Superscript => 44,
2611 VoidKind::Subscript => 45,
2612 VoidKind::Insert => 46,
2613 VoidKind::Delete => 47,
2614 VoidKind::DoubleQuoted => 48,
2615 VoidKind::SingleQuoted => 49,
2616 }
2617 }
2618}
2619
2620#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2622pub enum TextKind {
2623 Str,
2624 Symb,
2625 Verbatim,
2626 InlineMath,
2627 DisplayMath,
2628 Url,
2629 Email,
2630 FootnoteReference,
2631 CitationReference,
2634 SubstitutionReference,
2636 Comment,
2637 Doctype,
2638 Cdata,
2639}
2640
2641impl TextKind {
2642 fn to_c(self) -> c_int {
2643 match self {
2644 TextKind::Str => 25,
2645 TextKind::Symb => 29,
2646 TextKind::Verbatim => 30,
2647 TextKind::InlineMath => 32,
2648 TextKind::DisplayMath => 33,
2649 TextKind::Url => 34,
2650 TextKind::Email => 35,
2651 TextKind::FootnoteReference => 36,
2652 TextKind::CitationReference => 58,
2653 TextKind::SubstitutionReference => 59,
2654 TextKind::Comment => 52,
2655 TextKind::Doctype => 53,
2656 TextKind::Cdata => 55,
2657 }
2658 }
2659}
2660
2661#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2663pub enum BulletStyle {
2664 Dash,
2665 Plus,
2666 Star,
2667}
2668
2669impl BulletStyle {
2670 fn to_c(self) -> c_int {
2671 match self {
2672 BulletStyle::Dash => 0,
2673 BulletStyle::Plus => 1,
2674 BulletStyle::Star => 2,
2675 }
2676 }
2677}
2678
2679#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2681pub enum OrderedNumbering {
2682 Decimal,
2683 LowerAlpha,
2684 UpperAlpha,
2685 LowerRoman,
2686 UpperRoman,
2687}
2688
2689impl OrderedNumbering {
2690 fn to_c(self) -> c_int {
2691 match self {
2692 OrderedNumbering::Decimal => 0,
2693 OrderedNumbering::LowerAlpha => 1,
2694 OrderedNumbering::UpperAlpha => 2,
2695 OrderedNumbering::LowerRoman => 3,
2696 OrderedNumbering::UpperRoman => 4,
2697 }
2698 }
2699}
2700
2701#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2703pub enum OrderedDelim {
2704 Period,
2705 ParenAfter,
2706 ParenBoth,
2707}
2708
2709impl OrderedDelim {
2710 fn to_c(self) -> c_int {
2711 match self {
2712 OrderedDelim::Period => 0,
2713 OrderedDelim::ParenAfter => 1,
2714 OrderedDelim::ParenBoth => 2,
2715 }
2716 }
2717}
2718
2719#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2722pub enum Alignment {
2723 Default,
2724 Left,
2725 Right,
2726 Center,
2727}
2728
2729impl Alignment {
2730 fn to_c(self) -> c_int {
2731 match self {
2732 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
2733 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
2734 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
2735 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
2736 }
2737 }
2738
2739 fn from_c(v: c_int) -> Option<Self> {
2742 match v {
2743 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
2744 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
2745 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
2746 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
2747 _ => None,
2748 }
2749 }
2750}
2751
2752#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2754pub enum SmartPunctuation {
2755 LeftSingleQuote,
2756 RightSingleQuote,
2757 LeftDoubleQuote,
2758 RightDoubleQuote,
2759 Ellipses,
2760 EmDash,
2761 EnDash,
2762}
2763
2764impl SmartPunctuation {
2765 fn to_c(self) -> c_int {
2766 match self {
2767 SmartPunctuation::LeftSingleQuote => 0,
2768 SmartPunctuation::RightSingleQuote => 1,
2769 SmartPunctuation::LeftDoubleQuote => 2,
2770 SmartPunctuation::RightDoubleQuote => 3,
2771 SmartPunctuation::Ellipses => 4,
2772 SmartPunctuation::EmDash => 5,
2773 SmartPunctuation::EnDash => 6,
2774 }
2775 }
2776}
2777
2778#[derive(Clone, Debug, Eq, PartialEq)]
2793pub struct Warning {
2794 pub fidelity: Fidelity,
2795 pub path: String,
2802 pub kind: Kind,
2805}
2806
2807#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2809#[non_exhaustive]
2810pub enum Fidelity {
2811 Degraded,
2814 Dropped,
2816}
2817
2818impl Fidelity {
2819 fn from_c(v: c_int) -> Self {
2823 match v {
2824 ffi::TWIG_FIDELITY_DROPPED => Fidelity::Dropped,
2825 _ => Fidelity::Degraded,
2826 }
2827 }
2828}
2829
2830#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2831#[non_exhaustive]
2832pub enum ContainerOrigin {
2833 Element,
2835 Directive,
2839}
2840
2841impl ContainerOrigin {
2842 fn from_c(v: c_int) -> Option<Self> {
2845 match v {
2846 ffi::TWIG_CONTAINER_ORIGIN_ELEMENT => Some(ContainerOrigin::Element),
2847 ffi::TWIG_CONTAINER_ORIGIN_DIRECTIVE => Some(ContainerOrigin::Directive),
2848 _ => None,
2849 }
2850 }
2851}
2852
2853#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2855pub enum DirectiveForm {
2856 Text,
2857 Leaf,
2858 Container,
2859}
2860
2861impl DirectiveForm {
2862 fn to_c(self) -> c_int {
2863 match self {
2864 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
2865 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
2866 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
2867 }
2868 }
2869
2870 fn from_c(v: c_int) -> Option<Self> {
2874 match v {
2875 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
2876 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
2877 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
2878 _ => None,
2879 }
2880 }
2881}
2882
2883fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
2887 match s {
2888 Some(x) => (x.as_ptr(), x.len(), 1),
2889 None => (std::ptr::null(), 0, 0),
2890 }
2891}
2892
2893#[derive(Debug)]
2900pub struct Builder {
2901 raw: NonNull<ffi::TwigBuilder>,
2902}
2903
2904impl Builder {
2905 pub fn new() -> Result<Self, Error> {
2907 let mut raw = std::ptr::null_mut();
2908 let status = unsafe { ffi::twig_builder_create(&mut raw) };
2909 Error::from_status(status)?;
2910 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
2911 Ok(Self { raw })
2912 }
2913
2914 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
2917 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
2918 }
2919
2920 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
2922 self.emit(|b, out| unsafe {
2923 ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out)
2924 })
2925 }
2926
2927 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
2929 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
2930 }
2931
2932 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
2934 let (lp, ll, has) = opt_str(lang);
2935 self.emit(|b, out| unsafe {
2936 ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out)
2937 })
2938 }
2939
2940 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
2942 self.emit(|b, out| unsafe {
2943 ffi::twig_builder_add_raw_block(
2944 b,
2945 format.as_ptr(),
2946 format.len(),
2947 text.as_ptr(),
2948 text.len(),
2949 out,
2950 )
2951 })
2952 }
2953
2954 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
2956 self.emit(|b, out| unsafe {
2957 ffi::twig_builder_add_metadata(
2958 b,
2959 lang.as_ptr(),
2960 lang.len(),
2961 text.as_ptr(),
2962 text.len(),
2963 out,
2964 )
2965 })
2966 }
2967
2968 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
2970 self.emit(|b, out| unsafe {
2971 ffi::twig_builder_add_raw_inline(
2972 b,
2973 format.as_ptr(),
2974 format.len(),
2975 text.as_ptr(),
2976 text.len(),
2977 out,
2978 )
2979 })
2980 }
2981
2982 pub fn add_smart_punctuation(
2987 &mut self,
2988 kind: SmartPunctuation,
2989 text: &str,
2990 ) -> Result<NodeId, Error> {
2991 self.emit(|b, out| unsafe {
2992 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
2993 })
2994 }
2995
2996 pub fn add_link(
2999 &mut self,
3000 destination: Option<&str>,
3001 reference: Option<&str>,
3002 ) -> Result<NodeId, Error> {
3003 let (dp, dl, hd) = opt_str(destination);
3004 let (rp, rl, hr) = opt_str(reference);
3005 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
3006 }
3007
3008 pub fn add_image(
3010 &mut self,
3011 destination: Option<&str>,
3012 reference: Option<&str>,
3013 ) -> Result<NodeId, Error> {
3014 let (dp, dl, hd) = opt_str(destination);
3015 let (rp, rl, hr) = opt_str(reference);
3016 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
3017 }
3018
3019 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
3021 self.emit(|b, out| unsafe {
3022 ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out)
3023 })
3024 }
3025
3026 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
3028 self.emit(|b, out| unsafe {
3029 ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out)
3030 })
3031 }
3032
3033 pub fn add_processing_instruction(
3035 &mut self,
3036 target: &str,
3037 data: &str,
3038 ) -> Result<NodeId, Error> {
3039 self.emit(|b, out| unsafe {
3040 ffi::twig_builder_add_processing_instruction(
3041 b,
3042 target.as_ptr(),
3043 target.len(),
3044 data.as_ptr(),
3045 data.len(),
3046 out,
3047 )
3048 })
3049 }
3050
3051 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
3053 self.emit(|b, out| unsafe {
3054 ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out)
3055 })
3056 }
3057
3058 pub fn add_citation(&mut self, label: &str) -> Result<NodeId, Error> {
3063 self.emit(|b, out| unsafe {
3064 ffi::twig_builder_add_citation(b, label.as_ptr(), label.len(), out)
3065 })
3066 }
3067
3068 pub fn add_substitution(&mut self, label: &str) -> Result<NodeId, Error> {
3072 self.emit(|b, out| unsafe {
3073 ffi::twig_builder_add_substitution(b, label.as_ptr(), label.len(), out)
3074 })
3075 }
3076
3077 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
3079 self.emit(|b, out| unsafe {
3080 ffi::twig_builder_add_reference(
3081 b,
3082 label.as_ptr(),
3083 label.len(),
3084 destination.as_ptr(),
3085 destination.len(),
3086 out,
3087 )
3088 })
3089 }
3090
3091 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
3093 self.emit(|b, out| unsafe {
3094 ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out)
3095 })
3096 }
3097
3098 pub fn add_ordered_list(
3100 &mut self,
3101 numbering: OrderedNumbering,
3102 delim: OrderedDelim,
3103 tight: bool,
3104 start: Option<u32>,
3105 ) -> Result<NodeId, Error> {
3106 let (start_val, has_start) = match start {
3107 Some(s) => (s, 1),
3108 None => (0, 0),
3109 };
3110 self.emit(|b, out| unsafe {
3111 ffi::twig_builder_add_ordered_list(
3112 b,
3113 numbering.to_c(),
3114 delim.to_c(),
3115 tight as c_int,
3116 start_val,
3117 has_start,
3118 out,
3119 )
3120 })
3121 }
3122
3123 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
3125 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
3126 }
3127
3128 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
3130 self.emit(|b, out| unsafe {
3131 ffi::twig_builder_add_task_list_item(b, checked as c_int, out)
3132 })
3133 }
3134
3135 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
3137 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
3138 }
3139
3140 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
3142 self.emit(|b, out| unsafe {
3143 ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out)
3144 })
3145 }
3146
3147 pub fn add_cell_spanning(
3152 &mut self,
3153 head: bool,
3154 alignment: Alignment,
3155 colspan: u32,
3156 rowspan: u32,
3157 ) -> Result<NodeId, Error> {
3158 self.emit(|b, out| unsafe {
3159 ffi::twig_builder_add_cell_spanning(
3160 b,
3161 head as c_int,
3162 alignment.to_c(),
3163 colspan,
3164 rowspan,
3165 out,
3166 )
3167 })
3168 }
3169
3170 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
3173 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
3174 let status = unsafe {
3175 ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len())
3176 };
3177 Error::from_status(status)
3178 }
3179
3180 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
3184 let kvs: Vec<ffi::TwigKeyVal> = attrs
3185 .iter()
3186 .map(|(k, v)| ffi::TwigKeyVal {
3187 key: k.as_ptr(),
3188 key_len: k.len(),
3189 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
3190 value_len: v.map_or(0, |s| s.len()),
3191 })
3192 .collect();
3193 let status = unsafe {
3194 ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len())
3195 };
3196 Error::from_status(status)
3197 }
3198
3199 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3202 let raw = self.raw.as_ptr();
3203 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
3204 }
3205
3206 pub fn serialize_to(&mut self, root: NodeId, target: Target) -> Result<Vec<u8>, Error> {
3213 let raw = self.raw.as_ptr();
3214 let ffi_target: ffi::TwigFormat = target.into();
3215 collect_bytes(|ptr, len| unsafe {
3216 ffi::twig_builder_serialize(raw, root.0, ffi_target as i32, ptr, len)
3217 })
3218 }
3219
3220 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
3225 self.serialize_to(root, format.into())
3226 }
3227
3228 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3230 let raw = self.raw.as_ptr();
3231 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
3232 }
3233
3234 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
3237 let raw = self.raw.as_ptr();
3238 collect_matches(|ptr, len| unsafe {
3239 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
3240 })
3241 }
3242
3243 fn emit(
3246 &mut self,
3247 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
3248 ) -> Result<NodeId, Error> {
3249 let mut id: u32 = 0;
3250 let status = call(self.raw.as_ptr(), &mut id);
3251 Error::from_status(status)?;
3252 Ok(NodeId(id))
3253 }
3254}
3255
3256impl Drop for Builder {
3257 fn drop(&mut self) {
3258 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
3259 }
3260}
3261
3262#[cfg(test)]
3263mod tests {
3264 use super::*;
3265
3266 #[test]
3267 fn abi_version_matches() {
3268 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
3272 }
3273
3274 #[test]
3275 fn parses_and_renders_markdown_html() {
3276 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3277 let html = doc.render_html().expect("render html");
3278 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
3279 }
3280
3281 #[test]
3282 fn parses_html_input() {
3283 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
3284 let html = doc.render_html().expect("render html");
3285 assert!(String::from_utf8_lossy(&html).contains("hi"));
3286 }
3287
3288 #[test]
3289 fn parses_renders_and_writes_asciidoc() {
3290 let mut doc = Document::parse_str("= Title\n\nsome *bold* text\n", Format::Asciidoc)
3291 .expect("parse asciidoc");
3292 let html = String::from_utf8_lossy(&doc.render_html().expect("render html")).into_owned();
3293 assert!(html.contains("<h1>Title</h1>"), "got {html:?}");
3294 assert!(html.contains("<strong>bold</strong>"), "got {html:?}");
3295
3296 let back = doc.serialize_to(Target::Asciidoc).expect("serialize asciidoc");
3298 assert_eq!(String::from_utf8_lossy(&back), "= Title\n\nsome *bold* text\n");
3299 let mut md = Document::parse_str("# Title\n\nsome **bold** text\n", Format::Markdown)
3300 .expect("parse markdown");
3301 let converted = md.serialize_to(Target::Asciidoc).expect("convert to asciidoc");
3302 assert_eq!(String::from_utf8_lossy(&converted), "= Title\n\nsome *bold* text\n");
3303 assert_eq!(Target::from(Format::Asciidoc), Target::Asciidoc);
3304 assert_eq!(Target::Asciidoc.as_format(), Some(Format::Asciidoc));
3305 }
3306
3307 #[test]
3308 fn serialize_round_trips_and_cross_converts() {
3309 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3310
3311 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
3312 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
3313
3314 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
3316 }
3317
3318 #[test]
3319 fn serialize_markdown_to_djot() {
3320 let mut doc =
3321 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3322 let djot = doc.serialize(Format::Djot).expect("serialize djot");
3323 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3324 }
3325
3326 #[test]
3327 fn serialize_to_takes_the_output_axis() {
3328 let mut doc =
3329 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3330
3331 let djot = doc.serialize_to(Target::Djot).expect("serialize djot");
3332 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3333
3334 assert_eq!(doc.serialize_to(Target::Xml), Err(Error::UnsupportedFormat));
3337 }
3338
3339 #[test]
3340 fn serialize_and_serialize_to_agree() {
3341 let mut a = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3344 let mut b = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3345 for format in [Format::Markdown, Format::Djot, Format::Html] {
3346 assert_eq!(a.serialize(format), b.serialize_to(Target::from(format)));
3347 }
3348 }
3349
3350 #[test]
3351 fn every_format_is_a_target_that_names_it_back() {
3352 for format in [Format::Djot, Format::Markdown, Format::Xml, Format::Html] {
3355 assert_eq!(Target::from(format).as_format(), Some(format));
3356 }
3357 }
3358
3359 #[test]
3360 fn ast_json_dumps_the_tree() {
3361 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
3362 let json = doc.ast_json().expect("ast json");
3363 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
3364 }
3365
3366 #[test]
3367 fn query_finds_nodes_by_selector() {
3368 let source = "# One\n\n## Two\n";
3369 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3370 let matches = doc.query("heading").expect("query");
3371
3372 assert_eq!(matches.len(), 2);
3373 for m in &matches {
3374 assert_eq!(m.kind, Kind::Heading);
3375 assert!(m.span.start < m.span.end);
3376 }
3377 }
3378
3379 #[test]
3380 fn query_recovers_code_spans() {
3381 let source = "prose `code` more prose\n";
3382 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3383 let matches = doc.query("verbatim").expect("query");
3384
3385 assert_eq!(matches.len(), 1);
3386 assert_eq!(&source[matches[0].span.clone()], "`code`");
3387 }
3388
3389 #[test]
3390 fn document_span_accessors_read_by_node_id() {
3391 let source = "# hi\n\ntext\n";
3392 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3393 let heading = doc.query("heading").expect("query").pop().expect("heading");
3394
3395 assert_eq!(
3396 doc.span(NodeId(heading.node_id)).expect("span"),
3397 heading.span
3398 );
3399 assert_eq!(
3400 doc.content_span(NodeId(heading.node_id))
3401 .expect("content span"),
3402 heading.content_span
3403 );
3404 assert_eq!(doc.span(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3405 }
3406
3407 #[test]
3408 fn document_walks_its_tree_without_an_editor() {
3409 let source = "# hi\n\ntext\n";
3410 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3411
3412 let nodes = doc.nodes().expect("nodes");
3413 assert!(nodes.len() >= 3);
3414 for (i, n) in nodes.iter().enumerate() {
3415 assert_eq!(n.id, NodeId(i as u32));
3416 }
3417
3418 let kids = doc.children(None).expect("children");
3419 assert_eq!(kids.len(), 2);
3420 assert_eq!(kids[0].kind, Kind::Heading);
3421
3422 let sub = doc.subtree(NodeId(kids[0].node_id)).expect("subtree");
3423 assert_eq!(sub[0].id, NodeId(0));
3424 assert_eq!(sub[0].parent, None);
3425 assert_eq!(sub[0].span, kids[0].span);
3426
3427 let hit = doc.node_at(2).expect("node_at").expect("a node at 2");
3428 let chain = doc.ancestors_at(2).expect("ancestors");
3429 assert_eq!(chain.last().expect("deepest").node_id, hit.node_id);
3430 assert_eq!(chain[0].kind, Kind::Doc);
3431
3432 assert_eq!(doc.subtree(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3433 }
3434
3435 #[test]
3436 fn editor_document_view_reads_the_live_tree() {
3437 let mut ed = Editor::new_str("# one\n\ntwo\n", Format::Markdown).expect("editor");
3438
3439 {
3440 let mut view = ed.document().expect("view");
3441 let kids = view.children(None).expect("children");
3442 assert_eq!(kids.len(), 2);
3443 assert_eq!(kids[0].kind, Kind::Heading);
3444 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..5);
3445 assert_eq!(view.render_html(), Err(Error::UnsupportedFormat));
3447 assert_eq!(
3448 view.serialize(Format::Markdown),
3449 Err(Error::UnsupportedFormat)
3450 );
3451 }
3452
3453 ed.replace("0", "# one and a half").expect("replace");
3454 let mut view = ed.document().expect("view");
3455 let kids = view.children(None).expect("children");
3456 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..16);
3457 }
3458
3459 #[test]
3460 fn query_rejects_a_malformed_selector() {
3461 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
3462 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
3463 }
3464
3465 #[test]
3466 fn editor_edits_by_index_path() {
3467 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
3468 ed.replace_content("0.0", "bye").expect("replace_content");
3469 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
3470 }
3471
3472 #[test]
3473 fn flat_nodes_expose_element_name_and_attrs() {
3474 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
3478 let mut ed = Editor::new_ext(
3479 src.as_bytes(),
3480 Format::Markdown,
3481 MarkdownExtensions {
3482 html_elements: true,
3483 ..Default::default()
3484 },
3485 )
3486 .expect("editor");
3487 let nodes = ed.nodes().expect("nodes");
3488
3489 let source = nodes
3490 .iter()
3491 .find(|n| n.name.as_deref() == Some("source"))
3492 .expect("a <source> element node");
3493 assert_eq!(
3494 source.attrs,
3495 vec![
3496 (
3497 "media".to_string(),
3498 Some("(prefers-color-scheme: dark)".to_string())
3499 ),
3500 ("srcset".to_string(), Some("d.svg".to_string())),
3501 ]
3502 );
3503
3504 let img = nodes
3507 .iter()
3508 .find(|n| n.kind == Kind::Image)
3509 .expect("an image node");
3510 assert!(img.name.is_none());
3511 assert_eq!(img.destination.as_deref(), Some("l.svg"));
3512
3513 let picture_kids_str = nodes.iter().find(|n| n.kind == Kind::Str);
3515 if let Some(s) = picture_kids_str {
3516 assert!(s.name.is_none() && s.attrs.is_empty());
3517 }
3518 }
3519
3520 #[test]
3521 fn definitions_finds_what_a_walk_from_the_root_cannot() {
3522 let mut doc = Document::parse_str(
3526 "text[^1] [x][a]\n\n[^1]: note\n\n[a]: /u\n",
3527 Format::Markdown,
3528 )
3529 .expect("parse markdown");
3530
3531 let defs = doc.definitions().expect("definitions");
3532 let mut kinds: Vec<Kind> = defs.iter().map(|m| m.kind.clone()).collect();
3533 kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3534 assert_eq!(kinds, vec![Kind::Footnote, Kind::Reference]);
3535
3536 let all = doc.nodes().expect("nodes");
3539 let root = all
3540 .iter()
3541 .find(|n| n.kind == Kind::Doc)
3542 .expect("a doc root");
3543 let mut reachable = vec![root.id];
3544 let mut i = 0;
3545 while i < reachable.len() {
3546 let n = &all[reachable[i].0 as usize];
3547 let mut c = n.first_child;
3548 while let Some(cid) = c {
3549 reachable.push(cid);
3550 c = all[cid.0 as usize].next_sibling;
3551 }
3552 i += 1;
3553 }
3554 for d in &defs {
3555 assert!(
3556 !reachable.contains(&NodeId(d.node_id)),
3557 "{} should be unreachable from the root",
3558 d.kind
3559 );
3560 }
3561
3562 let mut plain = Document::parse_str("just text\n", Format::Markdown).expect("parse");
3564 assert_eq!(plain.definitions().expect("definitions"), Vec::new());
3565 }
3566
3567 #[test]
3568 fn kind_round_trips_through_its_published_name() {
3569 for k in [
3573 Kind::Doc,
3574 Kind::Para,
3575 Kind::Heading,
3576 Kind::Container,
3577 Kind::TaskListItem,
3578 Kind::Superscript,
3579 Kind::FootnoteReference,
3580 Kind::ProcessingInstruction,
3581 Kind::Cdata,
3582 ] {
3583 assert_eq!(Kind::from(k.as_str()), k, "{k} did not round-trip");
3584 assert!(!k.is_unknown());
3585 }
3586 }
3587
3588 #[test]
3589 fn an_unknown_kind_name_is_carried_rather_than_lost() {
3590 let k = Kind::from("some_future_kind");
3593 assert!(k.is_unknown());
3594 assert_eq!(k.as_str(), "some_future_kind");
3595 assert_eq!(k, Kind::Other("some_future_kind".to_string()));
3596 }
3597
3598 #[test]
3599 fn every_kind_the_library_publishes_has_a_variant() {
3600 let cases: &[(&str, Format, MarkdownExtensions)] = &[
3605 (
3606 "# 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",
3607 Format::Markdown,
3608 MarkdownExtensions {
3609 directives: false,
3610 math: false,
3611 html_elements: false,
3612 highlight: false,
3613 highlight_colors: false,
3614 },
3615 ),
3616 (
3617 "| 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",
3618 Format::Markdown,
3619 MarkdownExtensions::default(),
3620 ),
3621 (
3622 ":::note\nbody\n:::\n\n:role[x]\n\n$a+b$ ==h== ==🔴 r==\n",
3623 Format::Markdown,
3624 MarkdownExtensions {
3625 directives: true,
3626 math: true,
3627 html_elements: false,
3628 highlight: true,
3629 highlight_colors: true,
3630 },
3631 ),
3632 (
3633 "a^b^ c~d~ {=e=} {+f+} {-g-} 'q' \"dq\"\n\n\n\n<https://e.com>\n",
3634 Format::Djot,
3635 MarkdownExtensions::default(),
3636 ),
3637 (
3638 "<!-- c --><!DOCTYPE html><video controls><p>x</p></video>",
3639 Format::Html,
3640 MarkdownExtensions::default(),
3641 ),
3642 ];
3643
3644 let mut unknown: Vec<String> = Vec::new();
3645 let mut seen: Vec<String> = Vec::new();
3646 for (src, format, ext) in cases {
3647 let mut ed = Editor::new_ext(src.as_bytes(), *format, *ext).expect("editor");
3648 for n in ed.nodes().expect("nodes") {
3649 if n.kind.is_unknown() {
3650 unknown.push(n.kind.as_str().to_string());
3651 }
3652 seen.push(n.kind.as_str().to_string());
3653 }
3654 }
3655 unknown.sort();
3656 unknown.dedup();
3657 assert!(unknown.is_empty(), "kinds with no variant: {unknown:?}");
3658
3659 seen.sort();
3662 seen.dedup();
3663 assert!(
3664 seen.len() >= 30,
3665 "only {} distinct kinds reached: {seen:?}",
3666 seen.len()
3667 );
3668 }
3669
3670 #[test]
3671 fn diagnostics_report_what_a_conversion_would_lose() {
3672 let mut doc = Document::parse_str("a^b^ c\n", Format::Djot).expect("parse djot");
3676
3677 let to_md = doc
3678 .diagnostics(Target::Markdown)
3679 .expect("markdown diagnostics");
3680 assert_eq!(
3681 to_md,
3682 vec![Warning {
3683 fidelity: Fidelity::Degraded,
3684 path: "0/1".to_string(),
3685 kind: Kind::Superscript,
3686 }]
3687 );
3688
3689 assert_eq!(
3691 doc.diagnostics(Target::Djot).expect("djot diagnostics"),
3692 Vec::new()
3693 );
3694 }
3695
3696 #[test]
3697 fn diagnostics_separate_a_droppable_node_from_a_degradable_one() {
3698 let mut doc =
3702 Document::parse_str("<p>hi</p><!-- secret -->", Format::Html).expect("parse html");
3703 let warnings = doc.diagnostics(Target::Djot).expect("djot diagnostics");
3704 let comment = warnings
3705 .iter()
3706 .find(|w| w.kind == Kind::Comment)
3707 .expect("a warning about the comment");
3708 assert_eq!(comment.fidelity, Fidelity::Dropped);
3709 }
3710
3711 #[test]
3712 fn diagnostics_refuse_a_target_with_no_serializer() {
3713 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3716 assert_eq!(doc.diagnostics(Target::Xml), Err(Error::UnsupportedFormat));
3717 assert!(doc.diagnostics(Target::Asciidoc).is_ok());
3719 }
3720
3721 #[test]
3722 fn diagnostics_flag_a_header_less_table_and_leave_a_headed_one_alone() {
3723 let mut headed = Document::parse_str(
3728 "<table><tr><th>H</th></tr><tr><td>a</td></tr></table>",
3729 Format::Html,
3730 )
3731 .expect("parse headed table");
3732 assert!(
3733 headed
3734 .diagnostics(Target::Markdown)
3735 .expect("diagnostics")
3736 .iter()
3737 .all(|w| w.kind != Kind::Table)
3738 );
3739
3740 let mut headless = Document::parse_str("<table><tr><td>a</td></tr></table>", Format::Html)
3741 .expect("parse header-less table");
3742 let table_warning = headless
3743 .diagnostics(Target::Markdown)
3744 .expect("diagnostics")
3745 .into_iter()
3746 .find(|w| w.kind == Kind::Table)
3747 .expect("a warning about the table");
3748 assert_eq!(table_warning.fidelity, Fidelity::Degraded);
3749 }
3750
3751 #[test]
3752 fn container_origin_separates_a_div_from_a_div() {
3753 let mut html =
3758 Editor::new("<div>hi</div>\n".as_bytes(), Format::Html).expect("html editor");
3759 let mut md = Editor::new_ext(
3760 ":::div\nhi\n:::\n".as_bytes(),
3761 Format::Markdown,
3762 MarkdownExtensions {
3763 directives: true,
3764 ..Default::default()
3765 },
3766 )
3767 .expect("markdown editor");
3768
3769 let html_nodes = html.nodes().expect("html nodes");
3770 let md_nodes = md.nodes().expect("markdown nodes");
3771 let tag = html_nodes
3772 .iter()
3773 .find(|n| n.name.as_deref() == Some("div"))
3774 .expect("a <div> container");
3775 let directive = md_nodes
3776 .iter()
3777 .find(|n| n.name.as_deref() == Some("div"))
3778 .expect("a :::div container");
3779
3780 assert_eq!(tag.kind, directive.kind);
3782 assert_eq!(tag.name, directive.name);
3783 assert_eq!(tag.directive_form, directive.directive_form);
3784 assert_eq!(tag.directive_form, Some(DirectiveForm::Container));
3785
3786 assert_eq!(tag.origin, Some(ContainerOrigin::Element));
3788 assert_eq!(directive.origin, Some(ContainerOrigin::Directive));
3789 }
3790
3791 fn for_both_formats(src: &str, check: impl Fn(&mut Document, Format)) {
3795 for format in [Format::Markdown, Format::Djot] {
3796 let mut doc = Document::parse(src.as_bytes(), format).expect("parse");
3797 check(&mut doc, format);
3798 }
3799 }
3800
3801 #[test]
3802 fn marker_span_is_what_a_rich_view_hides() {
3803 for_both_formats("> - [x] done\n", |doc, format| {
3804 let nodes = doc.nodes().expect("nodes");
3805 let quote = nodes
3806 .iter()
3807 .find(|n| n.kind == Kind::BlockQuote)
3808 .expect("a block quote");
3809 let item = nodes
3810 .iter()
3811 .find(|n| n.kind == Kind::TaskListItem)
3812 .expect("a task item");
3813
3814 assert_eq!(quote.marker_span, Some(0..2), "{format:?}");
3818 assert_eq!(item.marker_span, Some(2..8), "{format:?}");
3819
3820 assert_eq!(quote.content_span, Some(quote.span.clone()), "{format:?}");
3824
3825 let para = nodes
3827 .iter()
3828 .find(|n| n.kind == Kind::Para)
3829 .expect("a paragraph");
3830 assert_eq!(para.marker_span, None, "{format:?}");
3831 });
3832 }
3833
3834 #[test]
3835 fn attrs_span_locates_the_attribute_block_a_heuristic_had_to_guess_at() {
3836 let src = "{.vis .family}\nheld back\n\nplain\n";
3842 let mut doc = Document::parse(src.as_bytes(), Format::Djot).expect("parse");
3843 let nodes = doc.nodes().expect("nodes");
3844 let paras: Vec<&FlatNode> = nodes.iter().filter(|n| n.kind == Kind::Para).collect();
3845 assert_eq!(paras.len(), 2);
3846
3847 let span = doc
3848 .attrs_span(paras[0].id)
3849 .expect("attrs span")
3850 .expect("the attributed paragraph has one");
3851 assert_eq!(&src[span.clone()], "{.vis .family}");
3852 assert!(span.end <= paras[0].span.start);
3855
3856 assert_eq!(doc.attrs_span(paras[1].id).expect("attrs span"), None);
3859 }
3860
3861 #[test]
3862 fn line_prefix_assembles_every_marker_on_the_line() {
3863 for_both_formats("> - [x] done\n", |doc, format| {
3864 assert_eq!(doc.line_prefix(9).expect("prefix"), Some(0..8), "{format:?}");
3867 });
3868 }
3869
3870 #[test]
3871 fn line_prefix_is_none_on_a_continuation_line() {
3872 for_both_formats("> c\n> d\n", |doc, format| {
3878 assert_eq!(doc.line_prefix(2).expect("prefix"), Some(0..2), "{format:?}");
3879 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
3880 });
3881 }
3882
3883 #[test]
3884 fn a_caret_at_a_blocks_end_is_in_that_block_in_both_formats() {
3885 for_both_formats("a\n\nb\n", |doc, format| {
3891 for offset in [0usize, 1, 3, 4] {
3892 let hit = doc
3893 .node_at_caret(offset)
3894 .expect("caret hit")
3895 .expect("some node");
3896 assert_eq!(hit.kind, Kind::Str, "{format:?} at {offset}");
3897 }
3898 for offset in [2usize, 5] {
3901 let hit = doc
3902 .node_at_caret(offset)
3903 .expect("caret hit")
3904 .expect("some node");
3905 assert_eq!(hit.kind, Kind::Doc, "{format:?} at {offset}");
3906 }
3907 });
3908 }
3909
3910 #[test]
3911 fn the_caret_chain_ends_at_the_node_the_scalar_call_returns() {
3912 for_both_formats("- a\n", |doc, format| {
3913 let hit = doc.node_at_caret(3).expect("hit").expect("some node");
3914 let chain = doc.ancestors_at_caret(3).expect("chain");
3915 assert_eq!(chain.last().map(|m| m.node_id), Some(hit.node_id), "{format:?}");
3916 assert!(
3919 chain.iter().any(|m| m.kind == Kind::ListItem),
3920 "{format:?}: chain should reach the list item"
3921 );
3922 });
3923 }
3924
3925 #[test]
3926 fn continuation_prefix_repeats_a_quote_and_indents_past_an_item() {
3927 for_both_formats("> - a\n", |doc, format| {
3928 assert_eq!(doc.line_prefix(4).expect("prefix"), Some(0..4), "{format:?}");
3933 let cont = doc.continuation_prefix(4).expect("continuation");
3934 assert_eq!(cont.text, "> ", "{format:?}");
3935 assert_eq!(cont.columns, 4, "{format:?}");
3936 });
3937 }
3938
3939 #[test]
3940 fn continuation_prefix_answers_on_a_line_that_opens_nothing() {
3941 for_both_formats("> c\n> d\n", |doc, format| {
3944 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
3945 assert_eq!(
3946 doc.continuation_prefix(6).expect("continuation").text,
3947 "> ",
3948 "{format:?}"
3949 );
3950 });
3951 }
3952
3953 #[test]
3954 fn continuation_prefix_takes_an_ordered_markers_own_width() {
3955 for_both_formats("10. x\n", |doc, format| {
3958 assert_eq!(
3959 doc.continuation_prefix(4).expect("continuation").columns,
3960 4,
3961 "{format:?}"
3962 );
3963 });
3964 for_both_formats("1. x\n", |doc, format| {
3965 assert_eq!(
3966 doc.continuation_prefix(3).expect("continuation").columns,
3967 3,
3968 "{format:?}"
3969 );
3970 });
3971 }
3972
3973 #[test]
3974 fn a_blank_line_keeps_a_quote_alive_and_drops_an_items_indent() {
3975 for_both_formats("> - a\n", |doc, format| {
3976 let blank = doc.blank_line_prefix(4).expect("blank");
3977 assert_eq!(blank.text, ">", "{format:?}");
3980 assert_eq!(blank.columns, 1, "{format:?}");
3981 });
3982 for_both_formats("- a\n", |doc, format| {
3985 assert_eq!(doc.blank_line_prefix(3).expect("blank").text, "", "{format:?}");
3986 });
3987 }
3988
3989 #[test]
3990 fn a_prefix_column_count_is_not_its_byte_length() {
3991 let mut doc = Document::parse("- x
3994".as_bytes(), Format::Markdown).expect("parse");
3995 let cont = doc.continuation_prefix(2).expect("continuation");
3996 assert_eq!(cont.columns, 4);
3997 }
3998
3999 #[test]
4000 fn set_block_opens_a_heading_on_a_blank_line() {
4001 for format in [Format::Markdown, Format::Djot] {
4002 let mut ed = Editor::new("a\n\n".as_bytes(), format).expect("editor");
4003 ed.set_block(3, BlockKind::Heading(2)).expect("set_block");
4004 assert_eq!(ed.source().expect("source"), b"a\n\n## ", "{format:?}");
4005 let nodes = ed.nodes().expect("nodes");
4009 assert!(
4010 nodes.iter().any(|n| n.kind == Kind::Heading),
4011 "{format:?}: should have parsed a heading"
4012 );
4013 }
4014 }
4015
4016 #[test]
4017 fn set_block_refuses_a_blank_line_inside_a_code_block() {
4018 for format in [Format::Markdown, Format::Djot] {
4022 let src = "```\nx\n\ny\n```\n";
4023 let mut ed = Editor::new(src.as_bytes(), format).expect("editor");
4024 let blank = src.find("\n\n").expect("a blank line") + 1;
4025 assert!(
4026 matches!(
4027 ed.set_block(blank, BlockKind::Heading(1)),
4028 Err(Error::NotEditable)
4029 ),
4030 "{format:?}"
4031 );
4032 assert_eq!(ed.source().expect("source"), src.as_bytes(), "{format:?}");
4033 }
4034 }
4035
4036 #[test]
4037 fn task_items_report_their_checkbox_state() {
4038 for_both_formats("- [ ] a\n- [x] b\n- [X] c\n- d\n", |doc, format| {
4042 let nodes = doc.nodes().expect("nodes");
4043 let states: Vec<Option<bool>> = nodes
4044 .iter()
4045 .filter(|n| matches!(n.kind, Kind::TaskListItem | Kind::ListItem))
4046 .map(|n| n.checked)
4047 .collect();
4048 assert_eq!(
4049 states,
4050 vec![Some(false), Some(true), Some(true), None],
4051 "{format:?}"
4052 );
4053
4054 for n in nodes.iter().filter(|n| n.kind == Kind::Para) {
4057 assert_eq!(n.checked, None, "{format:?}");
4058 }
4059 });
4060 }
4061
4062 #[test]
4063 fn an_editor_reaches_the_caret_reads_through_its_document_view() {
4064 let mut ed = Editor::new("- a\n".as_bytes(), Format::Markdown).expect("editor");
4069 let mut view = ed.document().expect("document view");
4070
4071 assert_eq!(view.line_prefix(3).expect("prefix"), Some(0..2));
4072 let hit = view.node_at_caret(3).expect("hit").expect("some node");
4073 assert_eq!(hit.kind, Kind::Str);
4074 }
4075
4076 #[test]
4077 fn container_origin_is_none_for_non_containers() {
4078 let mut ed = Editor::new("# hi\n\npara\n".as_bytes(), Format::Markdown).expect("editor");
4081 for n in ed.nodes().expect("nodes") {
4082 assert_eq!(n.origin, None, "{} should carry no origin", n.kind);
4083 }
4084 }
4085
4086 #[test]
4087 fn flat_nodes_expose_directive_name_and_form() {
4088 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
4094 let mut ed = Editor::new_ext(
4095 src.as_bytes(),
4096 Format::Markdown,
4097 MarkdownExtensions {
4098 directives: true,
4099 ..Default::default()
4100 },
4101 )
4102 .expect("editor");
4103 let nodes = ed.nodes().expect("nodes");
4104
4105 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
4106 .iter()
4107 .filter(|n| n.kind == Kind::Container)
4108 .map(|n| (n.name.as_deref(), n.directive_form))
4109 .collect();
4110 assert_eq!(
4111 forms,
4112 vec![
4113 (Some("note"), Some(DirectiveForm::Container)),
4114 (Some("embed"), Some(DirectiveForm::Leaf)),
4115 (Some("abbr"), Some(DirectiveForm::Text)),
4116 ]
4117 );
4118
4119 let embed = nodes
4122 .iter()
4123 .find(|n| n.name.as_deref() == Some("embed"))
4124 .expect("embed");
4125 assert_eq!(
4126 embed.attrs,
4127 vec![("src".to_string(), Some("demo.html".to_string()))]
4128 );
4129 let para = nodes.iter().find(|n| n.kind == Kind::Para).expect("a para");
4130 assert!(para.directive_form.is_none() && para.name.is_none());
4131 }
4132
4133 #[test]
4134 fn editor_insert_child_and_delete() {
4135 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
4136 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4137 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
4138 ed.delete("0.1").expect("delete");
4139 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
4140 }
4141
4142 #[test]
4143 fn editor_edits_by_selector() {
4144 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4145 ed.replace("heading(\"Two\")", "## Renamed")
4146 .expect("replace");
4147 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
4148 }
4149
4150 #[test]
4151 fn editor_locator_errors_are_distinct() {
4152 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
4153 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
4154 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
4155 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
4156 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
4158 }
4159
4160 #[test]
4161 fn editor_reparse_break_rolls_back() {
4162 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
4163 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
4164 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
4165 }
4166
4167 #[test]
4168 fn editor_leaf_content_is_not_editable() {
4169 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4170 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
4171 }
4172
4173 #[test]
4174 fn editor_query_reflects_current_tree() {
4175 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
4176 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4177 assert_eq!(ed.query("element").expect("query").len(), 3);
4179 let json = ed.ast_json().expect("ast_json");
4180 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
4181 }
4182
4183 #[test]
4186 fn editor_edit_range_types_backspaces_and_reports_change() {
4187 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4188
4189 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
4191 assert_eq!(ed.source_str().unwrap(), "aXb\n");
4192 assert_eq!(c.old, 1..1);
4193 assert_eq!(c.new, 1..2);
4194 assert_eq!(c.delta(), 1);
4195
4196 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
4198 assert_eq!(ed.source_str().unwrap(), "ab\n");
4199 assert_eq!(c2.old, 1..2);
4200 assert_eq!(c2.new, 1..1);
4201 assert_eq!(c2.delta(), -1);
4202 }
4203
4204 #[test]
4205 fn editor_edit_range_rejects_bad_ranges() {
4206 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4207 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"); }
4211
4212 #[test]
4213 fn editor_last_change_reports_locator_ops_too() {
4214 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4215 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed")
4218 .expect("replace");
4219 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
4220 let c = ed.last_change().expect("a change was recorded");
4221 assert_eq!(c.old, 7..13);
4223 assert_eq!(c.new, 7..17);
4224 }
4225
4226 #[test]
4227 fn editor_nodes_is_a_walkable_flat_tree() {
4228 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4229 let nodes = ed.nodes().expect("nodes");
4230 assert!(!nodes.is_empty());
4231
4232 for (i, n) in nodes.iter().enumerate() {
4234 assert_eq!(n.id, NodeId(i as u32));
4235 }
4236 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
4238 assert_eq!(roots.len(), 1);
4239 assert_eq!(roots[0].kind, Kind::Doc);
4240
4241 let heading = nodes
4243 .iter()
4244 .find(|n| n.kind == Kind::Heading)
4245 .expect("a heading");
4246 assert_eq!(heading.level, Some(1));
4247 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
4248
4249 assert_eq!(heading.head, None);
4251 assert_eq!(heading.alignment, None);
4252
4253 for n in nodes.iter().filter(|n| n.parent.is_some()) {
4256 let p = &nodes[n.parent.unwrap().0 as usize];
4257 let mut kid = p.first_child;
4258 let mut seen = false;
4259 while let Some(NodeId(k)) = kid {
4260 if k == n.id.0 {
4261 seen = true;
4262 break;
4263 }
4264 kid = nodes[k as usize].next_sibling;
4265 }
4266 assert!(
4267 seen,
4268 "node {:?} not found among its parent's children",
4269 n.id
4270 );
4271 }
4272 }
4273
4274 #[test]
4275 fn editor_child_spans_and_subtree_agree_with_nodes() {
4276 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
4277 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4278 let all = ed.nodes().expect("nodes");
4279 let doc = all.iter().find(|n| n.kind == Kind::Doc).expect("doc");
4280
4281 let top = ed.child_spans(None).expect("child_spans");
4284 let mut want = Vec::new();
4285 let mut c = doc.first_child;
4286 while let Some(id) = c {
4287 want.push(id);
4288 c = all[id.0 as usize].next_sibling;
4289 }
4290 assert_eq!(top.len(), want.len(), "top-level count");
4291 for (m, id) in top.iter().zip(&want) {
4292 assert_eq!(m.node_id, id.0, "child id");
4293 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
4294 assert_eq!(m.span, all[id.0 as usize].span, "child span");
4295 }
4296 assert!(
4298 src[top[0].span.clone()].starts_with('#'),
4299 "first block is the heading"
4300 );
4301
4302 let list = top
4304 .iter()
4305 .find(|m| {
4306 matches!(
4307 m.kind,
4308 Kind::BulletList | Kind::OrderedList | Kind::TaskList
4309 )
4310 })
4311 .expect("a list");
4312 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
4313 assert_eq!(items.len(), 2);
4314 assert!(
4315 items.iter().all(|m| m.kind == Kind::ListItem),
4316 "items: {items:?}"
4317 );
4318
4319 let para = top
4321 .iter()
4322 .find(|m| m.kind == Kind::Para)
4323 .expect("a para")
4324 .node_id;
4325 let sub = ed.subtree(NodeId(para)).expect("subtree");
4326 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
4327 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
4328 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
4329 assert_eq!(sub[0].kind, Kind::Para);
4330 for (i, n) in sub.iter().enumerate() {
4331 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
4332 for link in [n.parent, n.first_child, n.next_sibling]
4333 .into_iter()
4334 .flatten()
4335 {
4336 assert!(
4337 (link.0 as usize) < sub.len(),
4338 "link {link:?} escapes the subtree"
4339 );
4340 }
4341 }
4342 assert!(
4343 src[sub[0].span.clone()].starts_with("Hello"),
4344 "absolute span: {:?}",
4345 &src[sub[0].span.clone()]
4346 );
4347
4348 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<Kind> {
4350 let mut out = Vec::new();
4351 let mut stack = vec![root];
4352 while let Some(id) = stack.pop() {
4353 let n = &all[id.0 as usize];
4354 out.push(n.kind.clone());
4355 let mut c = n.first_child;
4356 while let Some(cid) = c {
4357 stack.push(cid);
4358 c = all[cid.0 as usize].next_sibling;
4359 }
4360 }
4361 out
4362 }
4363 let mut want_kinds = arena_kinds(&all, NodeId(para));
4364 let mut got_kinds: Vec<Kind> = sub.iter().map(|n| n.kind.clone()).collect();
4365 want_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4369 got_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4370 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
4371
4372 assert!(matches!(
4374 ed.subtree(NodeId(9999)),
4375 Err(Error::InvalidArgument)
4376 ));
4377 }
4378
4379 #[test]
4380 fn flat_nodes_carry_table_head_and_alignment() {
4381 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
4385 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4386 let nodes = ed.nodes().expect("nodes");
4387
4388 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Row).collect();
4389 assert_eq!(rows.len(), 2, "a header row and one body row");
4390 assert_eq!(rows[0].head, Some(true), "first row is the header");
4391 assert_eq!(rows[1].head, Some(false), "second row is a body row");
4392
4393 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Cell).collect();
4394 assert_eq!(cells.len(), 4);
4395 assert_eq!(cells[0].alignment, Some(Alignment::Left));
4397 assert_eq!(cells[1].alignment, Some(Alignment::Right));
4398 assert_eq!(cells[2].alignment, Some(Alignment::Left));
4399 assert_eq!(cells[3].alignment, Some(Alignment::Right));
4400 assert_eq!(cells[0].head, Some(true));
4402 assert_eq!(cells[2].head, Some(false));
4403
4404 let mut plain =
4407 Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
4408 let pnodes = plain.nodes().expect("nodes");
4409 let pcell = pnodes
4410 .iter()
4411 .find(|n| n.kind == Kind::Cell)
4412 .expect("a cell");
4413 assert_eq!(pcell.alignment, Some(Alignment::Default));
4414 }
4415
4416 #[test]
4417 fn cell_extent_reports_merged_cells_and_nothing_else() {
4418 let src = "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td><td>b</td></tr></table>";
4419 let mut doc = Document::parse_str(src, Format::Html).expect("parse");
4420 let cells: Vec<NodeId> = doc
4421 .nodes()
4422 .expect("nodes")
4423 .iter()
4424 .filter(|n| n.kind == Kind::Cell)
4425 .map(|n| n.id)
4426 .collect();
4427 assert_eq!(cells.len(), 2);
4428 assert_eq!(doc.cell_extent(cells[0]).expect("extent"), Some((2, 3)));
4429 assert_eq!(doc.cell_extent(cells[1]).expect("extent"), Some((1, 1)));
4431
4432 let mut pipe =
4434 Document::parse_str("| a |\n| --- |\n| b |\n", Format::Markdown).expect("parse");
4435 let pipe_cell = pipe
4436 .nodes()
4437 .expect("nodes")
4438 .iter()
4439 .find(|n| n.kind == Kind::Cell)
4440 .expect("a cell")
4441 .id;
4442 assert_eq!(pipe.cell_extent(pipe_cell).expect("extent"), Some((1, 1)));
4443
4444 let root = NodeId(0);
4446 assert_eq!(pipe.cell_extent(root).expect("extent"), None);
4447 }
4448
4449 #[test]
4450 fn builder_add_cell_spanning_renders_colspan_and_rowspan() {
4451 let mut b = Builder::new().expect("builder");
4452 let wide_text = b.add_text(TextKind::Str, "wide").expect("str");
4453 let wide = b
4454 .add_cell_spanning(false, Alignment::Default, 2, 3)
4455 .expect("cell");
4456 b.set_children(wide, &[wide_text]).expect("children");
4457 let plain_text = b.add_text(TextKind::Str, "one").expect("str");
4458 let plain = b.add_cell(false, Alignment::Default).expect("cell");
4459 b.set_children(plain, &[plain_text]).expect("children");
4460 let row = b.add_row(false).expect("row");
4461 b.set_children(row, &[wide, plain]).expect("children");
4462 let table = b.add(VoidKind::Table).expect("table");
4463 b.set_children(table, &[row]).expect("children");
4464
4465 let html = String::from_utf8(b.render_html(table).expect("html")).expect("utf-8");
4466 assert!(
4467 html.contains("<td colspan=\"2\" rowspan=\"3\">wide</td>"),
4468 "{html}"
4469 );
4470 assert!(html.contains("<td>one</td>"), "{html}");
4472
4473 assert!(matches!(
4475 b.add_cell_spanning(false, Alignment::Default, 0, 1),
4476 Err(Error::InvalidArgument)
4477 ));
4478 }
4479
4480 #[test]
4481 fn editor_node_at_and_ancestors_hit_test_offsets() {
4482 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4483
4484 let m = ed
4486 .node_at(2)
4487 .expect("node_at")
4488 .expect("a node covers offset 2");
4489 assert!(m.span.contains(&2));
4490
4491 let chain = ed.ancestors_at(2).expect("ancestors_at");
4493 assert!(!chain.is_empty());
4494 assert_eq!(chain[0].kind, Kind::Doc);
4495 assert_eq!(chain.last().unwrap().node_id, m.node_id);
4496
4497 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
4499 }
4500
4501 #[test]
4504 fn editor_wrap_and_toggle_inline_round_trip() {
4505 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4506
4507 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
4509 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
4510 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
4511
4512 ed.toggle_inline(4, 8, InlineKind::Strong)
4514 .expect("toggle off");
4515 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4516
4517 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
4519 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
4520 }
4521
4522 #[test]
4523 fn editor_inline_kind_support_is_format_specific() {
4524 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4526 assert_eq!(
4527 md.wrap_range(2, 6, InlineKind::Mark),
4528 Err(Error::UnsupportedFormat)
4529 );
4530
4531 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4533 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
4534 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
4535 }
4536
4537 #[test]
4538 fn editor_toggle_strips_verbatim_via_content_span() {
4539 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
4540 ed.toggle_inline(2, 8, InlineKind::Verbatim)
4542 .expect("toggle code off");
4543 assert_eq!(ed.source_str().unwrap(), "a code b\n");
4544
4545 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
4548 ed2.toggle_inline(2, 7, InlineKind::Verbatim)
4549 .expect("toggle multi off");
4550 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
4551 }
4552
4553 #[test]
4554 fn editor_set_block_switches_para_and_heading_levels() {
4555 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
4556
4557 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
4559 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
4560
4561 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
4563 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
4564
4565 ed.set_block(2, BlockKind::Paragraph).expect("to para");
4567 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
4568 }
4569
4570 #[test]
4571 fn editor_set_block_rejects_bad_level_and_format() {
4572 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4573 assert_eq!(
4574 md.set_block(0, BlockKind::Heading(9)),
4575 Err(Error::InvalidArgument)
4576 );
4577
4578 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4579 assert_eq!(
4580 xml.set_block(1, BlockKind::Heading(1)),
4581 Err(Error::UnsupportedFormat)
4582 );
4583 }
4584
4585 #[test]
4586 fn editor_toggle_block_container_round_trips() {
4587 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
4588
4589 let c = ed
4590 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
4591 .expect("quote on");
4592 assert_eq!(ed.source_str().unwrap(), "> a\n");
4593 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
4594
4595 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4596 .expect("quote off");
4597 assert_eq!(ed.source_str().unwrap(), "a\n");
4598 }
4599
4600 #[test]
4601 fn editor_toggle_block_container_nests_a_partial_selection() {
4602 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
4603
4604 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
4607 .expect("nest");
4608 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
4609
4610 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
4612 .expect("peel");
4613 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
4614 }
4615
4616 #[test]
4617 fn editor_toggle_block_container_numbers_and_converts_lists() {
4618 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
4619
4620 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
4622 .expect("ordered on");
4623 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
4624
4625 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
4627 .expect("convert");
4628 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
4629 }
4630
4631 #[test]
4632 fn editor_toggle_block_container_rejects_unspellable_format() {
4633 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4634 assert_eq!(
4635 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
4636 Err(Error::UnsupportedFormat)
4637 );
4638 }
4639
4640 #[test]
4641 fn editor_insert_link_wraps_and_repoints() {
4642 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4643
4644 ed.insert_link(2, 6, "http://x.dev").expect("link");
4645 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
4646
4647 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
4649 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
4650 }
4651
4652 #[test]
4653 fn editor_insert_link_repoints_an_autolink() {
4654 for format in [Format::Markdown, Format::Djot] {
4659 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
4660 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
4661 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
4662
4663 let nodes = ed.nodes().expect("nodes");
4665 let url = nodes
4666 .iter()
4667 .find(|n| n.kind == Kind::Url)
4668 .expect("still an autolink");
4669 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
4670 assert!(!nodes.iter().any(|n| n.kind == Kind::Link));
4671 }
4672 }
4673
4674 #[test]
4675 fn editor_insert_link_escapes_the_destination() {
4676 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4679 dj.insert_link(0, 1, "a)b").expect("link");
4680 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
4681
4682 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4686 md.insert_link(0, 1, "a b").expect("link");
4687 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
4688
4689 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
4690 dj2.insert_link(0, 1, "a b").expect("link");
4691 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
4692 }
4693
4694 #[test]
4695 fn editor_insert_image_escapes_the_destination_per_format() {
4696 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
4699 md.insert_image(0, 1, "my cat.png").expect("image");
4700 assert_eq!(md.source_str().unwrap(), "\n");
4701
4702 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
4703 dj.insert_image(0, 1, "my cat.png").expect("image");
4704 assert_eq!(dj.source_str().unwrap(), "\n");
4705
4706 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
4708 paren.insert_image(0, 1, "a)b.png").expect("image");
4709 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
4710 }
4711
4712 #[test]
4713 fn editor_insert_image_keeps_an_empty_alt_empty() {
4714 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4717 ed.insert_image(1, 1, "cat.png").expect("image");
4718 assert_eq!(ed.source_str().unwrap(), "ab\n");
4719 }
4720
4721 #[test]
4722 fn editor_insert_image_rejects_a_newline_destination() {
4723 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
4724 assert_eq!(
4725 ed.insert_image(0, 1, "a\nb.png"),
4726 Err(Error::InvalidArgument)
4727 );
4728
4729 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4730 assert_eq!(
4731 xml.insert_image(3, 5, "x.png"),
4732 Err(Error::UnsupportedFormat)
4733 );
4734 }
4735
4736 #[test]
4737 fn editor_insert_link_rejects_a_newline_destination() {
4738 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
4739 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
4740
4741 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4742 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
4743 }
4744
4745 #[test]
4746 fn editor_insert_literal_keeps_typed_specials_literal() {
4747 for format in [Format::Markdown, Format::Djot] {
4748 let mut ed = Editor::new_str("z\n", format).expect("editor");
4749 ed.insert_literal(0, "*hi*").expect("literal");
4751
4752 let nodes = ed.nodes().expect("nodes");
4754 assert!(
4755 !nodes
4756 .iter()
4757 .any(|n| n.kind == Kind::Emph || n.kind == Kind::Strong)
4758 );
4759 let text: String = nodes
4760 .iter()
4761 .filter(|n| n.kind == Kind::Str)
4762 .filter_map(|n| n.text.clone())
4763 .collect();
4764 assert_eq!(text, "*hi*z");
4765 }
4766 }
4767
4768 #[test]
4769 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
4770 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
4772 ed.insert_literal(1, "# ").expect("literal");
4773 assert_eq!(ed.source_str().unwrap(), "a# z\n");
4774
4775 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
4777 ed2.insert_literal(0, "# ").expect("literal");
4778 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
4779 assert!(
4780 !ed2.nodes()
4781 .expect("nodes")
4782 .iter()
4783 .any(|n| n.kind == Kind::Heading)
4784 );
4785 }
4786
4787 #[test]
4788 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
4789 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4790 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
4791
4792 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4793 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
4794 }
4795
4796 #[test]
4797 fn editor_insert_line_break_splices_in_cell_br() {
4798 let mut ed =
4799 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
4800 ed.insert_line_break(3).expect("line break");
4802 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
4803 let nodes = ed.nodes().expect("nodes");
4805 assert!(nodes.iter().any(|n| n.kind == Kind::HardBreak));
4806 assert!(!nodes.iter().any(|n| n.kind == Kind::RawInline));
4807 }
4808
4809 #[test]
4810 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
4811 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
4813 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
4814
4815 let mut dj = Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
4817 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
4818
4819 let mut ed =
4821 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
4822 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
4823 }
4824
4825 #[test]
4826 fn editor_insert_thematic_break_is_blank_separated_per_format() {
4827 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
4831 md.insert_thematic_break(0).expect("rule");
4832 assert_eq!(md.source_str().unwrap(), "a\n\n---\n");
4833 let nodes = md.nodes().expect("nodes");
4834 assert!(nodes.iter().any(|n| n.kind == Kind::ThematicBreak));
4835 assert!(!nodes.iter().any(|n| n.kind == Kind::Heading));
4836
4837 let mut dj = Editor::new_str("a\n", Format::Djot).expect("editor");
4840 dj.insert_thematic_break(0).expect("rule");
4841 assert_eq!(dj.source_str().unwrap(), "a\n\n* * *\n");
4842
4843 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4844 assert_eq!(xml.insert_thematic_break(3), Err(Error::UnsupportedFormat));
4845 }
4846
4847 #[test]
4848 fn editor_split_block_keeps_both_halves_the_same_kind() {
4849 let mut item = Editor::new_str("- this is a list item\n", Format::Markdown).expect("editor");
4852 item.split_block(10).expect("split");
4853 assert_eq!(item.source_str().unwrap(), "- this is \n- a list item\n");
4854 let nodes = item.nodes().expect("nodes");
4855 assert_eq!(nodes.iter().filter(|n| n.kind == Kind::ListItem).count(), 2);
4856
4857 let mut tail = Editor::new_str("- a\n", Format::Markdown).expect("editor");
4859 tail.split_block(3).expect("split");
4860 assert_eq!(tail.source_str().unwrap(), "- a\n- \n");
4861
4862 let mut para = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4864 para.split_block(1).expect("split");
4865 assert_eq!(para.source_str().unwrap(), "a\n\nb\n");
4866
4867 let mut table =
4869 Editor::new_str("| a | b |\n|---|---|\n| c | d |\n", Format::Markdown).expect("editor");
4870 assert_eq!(table.split_block(3), Err(Error::NotEditable));
4871
4872 let mut empty = Editor::new_str("", Format::Markdown).expect("editor");
4873 assert_eq!(empty.split_block(0), Err(Error::NotFound));
4874 }
4875
4876 #[test]
4877 fn editor_toggle_code_block_round_trips_and_measures_the_fence() {
4878 let mut ed = Editor::new_str("a\n", Format::Markdown).expect("editor");
4879 ed.toggle_code_block(0, 1, Some("zig")).expect("fence");
4880 assert_eq!(ed.source_str().unwrap(), "```zig\na\n```\n");
4881 let nodes = ed.nodes().expect("nodes");
4882 assert!(nodes.iter().any(|n| n.kind == Kind::CodeBlock));
4883
4884 ed.toggle_code_block(0, 0, None).expect("unfence");
4885 assert_eq!(ed.source_str().unwrap(), "a\n");
4886
4887 let mut runs = Editor::new_str("a ``` b\n", Format::Markdown).expect("editor");
4890 runs.toggle_code_block(0, 7, None).expect("fence");
4891 assert_eq!(runs.source_str().unwrap(), "````\na ``` b\n````\n");
4892 }
4893
4894 #[test]
4895 fn editor_toggle_code_block_refuses_inside_a_list_item() {
4896 let mut ed = Editor::new_str("- a\n- b\n", Format::Markdown).expect("editor");
4899 assert_eq!(ed.toggle_code_block(2, 3, None), Err(Error::NotEditable));
4900 assert_eq!(ed.source_str().unwrap(), "- a\n- b\n");
4901 }
4902
4903 #[test]
4904 fn editor_set_code_language_retags_clears_and_refuses() {
4905 let mut ed = Editor::new_str("```zig\na\n```\n", Format::Markdown).expect("editor");
4906 ed.set_code_language(0, Some("rust")).expect("retag");
4907 assert_eq!(ed.source_str().unwrap(), "```rust\na\n```\n");
4908
4909 ed.set_code_language(0, None).expect("clear");
4912 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
4913 ed.set_code_language(0, Some("")).expect("empty");
4914 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
4915
4916 assert_eq!(
4919 ed.set_code_language(0, Some("a b")),
4920 Err(Error::InvalidArgument)
4921 );
4922 let mut dj = Editor::new_str("```\na\n```\n", Format::Djot).expect("editor");
4924 dj.set_code_language(0, Some("a b"))
4925 .expect("djot info string");
4926 assert_eq!(dj.source_str().unwrap(), "```a b\na\n```\n");
4927
4928 let mut para = Editor::new_str("x\n", Format::Markdown).expect("editor");
4929 assert_eq!(para.set_code_language(0, Some("zig")), Err(Error::NotFound));
4930 }
4931
4932 #[test]
4933 fn editor_task_checkbox_gestures() {
4934 let mut ed = Editor::new_str("- a\n", Format::Markdown).expect("editor");
4935
4936 ed.toggle_task_item(2).expect("add box");
4939 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
4940 assert!(
4941 ed.nodes()
4942 .unwrap()
4943 .iter()
4944 .any(|n| n.kind == Kind::TaskListItem)
4945 );
4946
4947 ed.set_task_checked(6, true).expect("tick");
4948 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
4949 ed.set_task_checked(6, true).expect("no-op");
4951 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
4952
4953 ed.toggle_task_checked(6).expect("flip");
4954 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
4955
4956 ed.toggle_task_item(6).expect("remove box");
4957 assert_eq!(ed.source_str().unwrap(), "- a\n");
4958
4959 assert_eq!(ed.set_task_checked(2, true), Err(Error::NotEditable));
4962 let mut para = Editor::new_str("a\n", Format::Markdown).expect("editor");
4964 assert_eq!(para.toggle_task_item(0), Err(Error::NotFound));
4965 }
4966
4967 #[test]
4968 fn editor_insert_footnote_writes_both_halves_as_one_edit() {
4969 for format in [Format::Markdown, Format::Djot] {
4970 let mut ed = Editor::new_str("see\n", format).expect("editor");
4971 ed.insert_footnote(3, "a").expect("footnote");
4972 assert_eq!(ed.source_str().unwrap(), "see[^a]\n\n[^a]: \n");
4973
4974 let nodes = ed.nodes().expect("nodes");
4976 assert!(nodes.iter().any(|n| n.kind == Kind::FootnoteReference));
4977 assert!(nodes.iter().any(|n| n.kind == Kind::Footnote));
4978
4979 ed.undo().expect("undo");
4981 assert_eq!(ed.source_str().unwrap(), "see\n");
4982 }
4983 }
4984
4985 #[test]
4986 fn editor_insert_footnote_reuses_an_existing_definition() {
4987 let mut ed = Editor::new_str("see\n", Format::Markdown).expect("editor");
4988 ed.insert_footnote(3, "a").expect("first");
4989 ed.insert_footnote(7, "a").expect("second reference");
4990 assert_eq!(ed.source_str().unwrap(), "see[^a][^a]\n\n[^a]: \n");
4991 let defs = ed
4992 .nodes()
4993 .unwrap()
4994 .iter()
4995 .filter(|n| n.kind == Kind::Footnote)
4996 .count();
4997 assert_eq!(defs, 1);
4998
4999 assert_eq!(ed.insert_footnote(3, ""), Err(Error::InvalidArgument));
5000 assert_eq!(ed.insert_footnote(3, "a]b"), Err(Error::InvalidArgument));
5001 }
5002
5003 #[test]
5004 fn editor_undo_redo_round_trip() {
5005 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5006 ed.edit_range(5, 5, "!").expect("edit");
5007 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5008
5009 let change = ed.undo().expect("undo ok").expect("something to undo");
5010 assert_eq!(ed.source_str().unwrap(), "hello\n");
5011 assert_eq!(change.new.end, 5);
5012 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
5013
5014 ed.redo().expect("redo ok").expect("something to redo");
5015 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5016 }
5017
5018 #[test]
5019 fn editor_coalesce_folds_a_run() {
5020 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5021 ed.edit_range(0, 0, "a").expect("edit");
5022 ed.edit_range(1, 1, "b").expect("edit");
5023 ed.coalesce_last_undo().expect("coalesce");
5024 assert_eq!(ed.source_str().unwrap(), "ab\n");
5025 ed.undo().expect("undo ok").expect("something to undo");
5027 assert_eq!(ed.source_str().unwrap(), "\n");
5028 assert!(ed.undo().expect("undo ok").is_none());
5029 }
5030
5031 #[test]
5032 fn editor_revision_bumps_per_successful_mutation() {
5033 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
5034 assert_eq!(ed.revision(), 0);
5035 ed.edit_range(1, 1, "y").expect("edit");
5036 assert_eq!(ed.revision(), 1);
5037
5038 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
5040 assert_eq!(xml.revision(), 0);
5041 assert!(xml.replace_content("0", "<b>").is_err());
5042 assert_eq!(xml.revision(), 0);
5043
5044 ed.undo().expect("undo ok").expect("something to undo");
5046 assert_eq!(ed.revision(), 2);
5047 ed.redo().expect("redo ok").expect("something to redo");
5048 assert_eq!(ed.revision(), 3);
5049 }
5050
5051 #[test]
5052 fn editor_dirty_range_tracks_and_clears() {
5053 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
5054 assert_eq!(ed.dirty_range(), None);
5056
5057 ed.edit_range(2, 2, "XY").expect("edit");
5059 assert_eq!(ed.dirty_range(), Some(2..4));
5060
5061 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
5065 assert!(
5066 d.start <= 2 && d.end >= 10,
5067 "range {d:?} must cover both edits"
5068 );
5069
5070 let rev = ed.revision();
5072 ed.clear_dirty();
5073 assert_eq!(ed.dirty_range(), None);
5074 assert_eq!(ed.revision(), rev);
5075
5076 ed.undo().expect("undo ok").expect("something to undo");
5078 assert!(ed.dirty_range().is_some());
5079 }
5080
5081 #[test]
5082 fn editor_caret_blob_follows_undo_and_redo() {
5083 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5084 assert!(ed.caret_blob().unwrap().is_empty());
5085
5086 ed.set_caret_blob(b"before").expect("set caret");
5088 ed.edit_range(5, 5, "!").expect("edit");
5089 assert!(ed.caret_blob().unwrap().is_empty());
5091 ed.set_caret_blob(b"after").expect("set caret");
5092
5093 ed.undo().expect("undo ok").expect("something to undo");
5095 assert_eq!(ed.source_str().unwrap(), "hello\n");
5096 assert_eq!(ed.caret_blob().unwrap(), b"before");
5097
5098 ed.redo().expect("redo ok").expect("something to redo");
5100 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5101 assert_eq!(ed.caret_blob().unwrap(), b"after");
5102 }
5103
5104 #[test]
5105 fn editor_coalesced_run_keeps_the_pre_run_caret() {
5106 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5107 ed.set_caret_blob(b"c0").expect("set caret");
5108 ed.edit_range(0, 0, "a").expect("edit");
5109 ed.set_caret_blob(b"c1").expect("set caret");
5110 ed.edit_range(1, 1, "b").expect("edit");
5111 ed.coalesce_last_undo().expect("coalesce");
5112 ed.set_caret_blob(b"c2").expect("set caret");
5113
5114 ed.undo().expect("undo ok").expect("something to undo");
5116 assert_eq!(ed.source_str().unwrap(), "\n");
5117 assert_eq!(ed.caret_blob().unwrap(), b"c0");
5118 }
5119
5120 #[test]
5121 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
5122 let mut ed = Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
5123 ed.renumber_ordered_lists(0).expect("renumber ok");
5124 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
5125 }
5126
5127 #[test]
5128 fn editor_renumber_ordered_lists_leaves_djot_prose_alone() {
5129 let src = "1. a\n 2. b\n2. c\n";
5132 let mut dj = Editor::new_str(src, Format::Djot).expect("editor");
5133 dj.renumber_ordered_lists(0).expect("renumber ok");
5134 assert_eq!(dj.source_str().unwrap(), src);
5135
5136 let mut md = Editor::new_str(src, Format::Markdown).expect("editor");
5137 md.renumber_ordered_lists(0).expect("renumber ok");
5138 assert_eq!(md.source_str().unwrap(), "1. a\n 1. b\n2. c\n");
5139 }
5140
5141 #[test]
5142 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
5143 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
5144 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
5145 }
5146
5147 #[test]
5148 fn editor_table_insert_row_and_set_alignment() {
5149 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
5150 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
5151 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
5153 ed.source_str().unwrap(),
5154 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
5155 );
5156 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
5158 }
5159
5160 #[test]
5161 fn editor_table_edit_off_a_table_is_not_found() {
5162 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
5163 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
5164 }
5165
5166 #[test]
5167 fn editor_set_block_converts_setext_heading() {
5168 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
5170 ed.set_block(0, BlockKind::Heading(1))
5171 .expect("setext to atx");
5172 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
5173 }
5174
5175 #[test]
5176 fn editor_unwrap_and_smart_delete() {
5177 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
5178 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
5180
5181 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
5182 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
5184 }
5185
5186 #[test]
5187 fn editor_directives_require_the_extension_flag() {
5188 let src = ":::vis{.public}\nhi\n:::\n";
5189 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
5192 assert_eq!(plain.query("directive").expect("query").len(), 0);
5193 let mut ext = Editor::new_ext(
5195 src.as_bytes(),
5196 Format::Markdown,
5197 MarkdownExtensions {
5198 directives: true,
5199 ..Default::default()
5200 },
5201 )
5202 .expect("editor");
5203 assert_eq!(ext.query("directive").expect("query").len(), 1);
5204 }
5205
5206 #[test]
5207 fn document_html_elements_make_embedded_img_queryable() {
5208 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
5209 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
5211 assert_eq!(plain.query("image").expect("query").len(), 0);
5212 let mut ext = Document::parse_str_with(
5214 src,
5215 Format::Markdown,
5216 MarkdownExtensions {
5217 html_elements: true,
5218 ..Default::default()
5219 },
5220 )
5221 .expect("parse");
5222 let images = ext.query("image").expect("query");
5223 assert_eq!(images.len(), 1);
5224 assert_eq!(images[0].kind, Kind::Image);
5225 }
5226
5227 #[test]
5228 fn editor_filter_public_audience_view() {
5229 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
5230 let mut ed = Editor::new_ext(
5231 src.as_bytes(),
5232 Format::Markdown,
5233 MarkdownExtensions {
5234 directives: true,
5235 ..Default::default()
5236 },
5237 )
5238 .expect("editor");
5239 ed.filter(
5241 "directive[name=vis]",
5242 Some("directive[class~=public]"),
5243 true,
5244 )
5245 .expect("filter");
5246 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
5247 }
5248
5249 #[test]
5250 fn editor_filter_rejects_a_malformed_selector() {
5251 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
5252 assert_eq!(
5253 ed.filter("list >", None, false),
5254 Err(Error::InvalidArgument)
5255 );
5256 }
5257
5258 #[test]
5259 fn builder_builds_and_renders_a_document() {
5260 let mut b = Builder::new().expect("builder");
5261
5262 let title = b.add_text(TextKind::Str, "Title").unwrap();
5264 let heading = b.add_heading(1).unwrap();
5265 b.set_children(heading, &[title]).unwrap();
5266
5267 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
5268 let world = b.add_text(TextKind::Str, "world").unwrap();
5269 let emph = b.add(VoidKind::Emph).unwrap();
5270 b.set_children(emph, &[world]).unwrap();
5271 let para = b.add(VoidKind::Para).unwrap();
5272 b.set_children(para, &[hello, emph]).unwrap();
5273
5274 let doc = b.add(VoidKind::Doc).unwrap();
5275 b.set_children(doc, &[heading, para]).unwrap();
5276
5277 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
5278 assert!(html.contains("<h1>Title</h1>"), "{html}");
5279 assert!(html.contains("<em>world</em>"), "{html}");
5280
5281 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5282 assert!(md.contains("# Title"), "{md}");
5283 assert!(md.contains("*world*"), "{md}");
5284
5285 let matches = b.query(doc, "heading").unwrap();
5286 assert_eq!(matches.len(), 1);
5287 assert_eq!(matches[0].kind, Kind::Heading);
5288
5289 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
5290 assert!(json.contains("\"kind\": \"doc\""), "{json}");
5291 }
5292
5293 #[test]
5294 fn builder_element_with_attributes() {
5295 let mut b = Builder::new().expect("builder");
5296 let inner = b.add_text(TextKind::Str, "hi").unwrap();
5297 let el = b.add_element("section").unwrap();
5298 b.set_children(el, &[inner]).unwrap();
5299 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)])
5300 .unwrap();
5301
5302 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
5303 assert!(html.contains("<section"), "{html}");
5304 assert!(html.contains("class=\"note\""), "{html}");
5305 assert!(html.contains("hidden"), "{html}");
5306 }
5307
5308 #[test]
5309 fn builder_lists_round_trip_to_markdown() {
5310 let mut b = Builder::new().expect("builder");
5311
5312 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
5314 let one_para = b.add(VoidKind::Para).unwrap();
5315 b.set_children(one_para, &[one_txt]).unwrap();
5316 let one = b.add(VoidKind::ListItem).unwrap();
5317 b.set_children(one, &[one_para]).unwrap();
5318
5319 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
5320 let two_para = b.add(VoidKind::Para).unwrap();
5321 b.set_children(two_para, &[two_txt]).unwrap();
5322 let two = b.add(VoidKind::ListItem).unwrap();
5323 b.set_children(two, &[two_para]).unwrap();
5324
5325 let list = b
5326 .add_ordered_list(
5327 OrderedNumbering::Decimal,
5328 OrderedDelim::Period,
5329 true,
5330 Some(1),
5331 )
5332 .unwrap();
5333 b.set_children(list, &[one, two]).unwrap();
5334 let doc = b.add(VoidKind::Doc).unwrap();
5335 b.set_children(doc, &[list]).unwrap();
5336
5337 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5338 assert!(md.contains("1. one"), "{md}");
5339 assert!(md.contains("2. two"), "{md}");
5340 }
5341
5342 #[test]
5343 fn builder_rejects_invalid_kind_and_id() {
5344 let b = Builder::new().expect("builder");
5345 let mut id = 0u32;
5349 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
5350 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5351
5352 let mut ptr = std::ptr::null();
5354 let mut len = 0usize;
5355 let status =
5356 unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
5357 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5358 }
5359
5360 fn all_gestures() -> Vec<Gesture> {
5364 let inline = [
5365 InlineKind::Strong,
5366 InlineKind::Emph,
5367 InlineKind::Verbatim,
5368 InlineKind::Mark,
5369 InlineKind::Superscript,
5370 InlineKind::Subscript,
5371 InlineKind::Insert,
5372 InlineKind::Delete,
5373 ];
5374 let mut all: Vec<Gesture> = Vec::new();
5375 for k in inline {
5376 all.push(Gesture::WrapRange(k));
5377 all.push(Gesture::ToggleInline(k));
5378 }
5379 for k in [
5380 BlockContainerKind::BlockQuote,
5381 BlockContainerKind::BulletList,
5382 BlockContainerKind::OrderedList,
5383 ] {
5384 all.push(Gesture::ToggleBlockContainer(k));
5385 }
5386 all.extend([
5387 Gesture::SetBlock,
5388 Gesture::InsertThematicBreak,
5389 Gesture::ToggleCodeBlock,
5390 Gesture::SetCodeLanguage,
5391 Gesture::ToggleTaskItem,
5392 Gesture::SetTaskChecked,
5393 Gesture::ToggleTaskChecked,
5394 Gesture::InsertLink,
5395 Gesture::InsertImage,
5396 Gesture::InsertFootnote,
5397 Gesture::InsertLiteral,
5398 Gesture::InsertLineBreak,
5399 Gesture::SplitBlock,
5400 Gesture::RenumberOrderedLists,
5401 Gesture::TableInsertRow,
5402 Gesture::TableDeleteRow,
5403 Gesture::TableInsertColumn,
5404 Gesture::TableDeleteColumn,
5405 Gesture::TableSetAlignment,
5406 Gesture::TableMoveRow,
5407 Gesture::TableMoveColumn,
5408 ]);
5409 all
5410 }
5411
5412 #[test]
5413 fn the_wire_space_ends_where_the_sweep_does() {
5414 let mut codes: Vec<c_int> = all_gestures().iter().map(|g| g.to_c().0).collect();
5420 codes.sort_unstable();
5421 codes.dedup();
5422 assert_eq!(codes, (0..=23).collect::<Vec<c_int>>());
5423
5424 let mut supported = -1;
5425 for code in &codes {
5426 let status = unsafe {
5427 ffi::twig_format_supports(
5428 ffi::TwigFormat::from(Format::Markdown) as c_int,
5429 *code,
5430 0,
5431 &mut supported,
5432 )
5433 };
5434 assert_eq!(Error::from_status(status), Ok(()), "code {code} did not decode");
5435 }
5436 let status = unsafe {
5438 ffi::twig_format_supports(
5439 ffi::TwigFormat::from(Format::Markdown) as c_int,
5440 24,
5441 0,
5442 &mut supported,
5443 )
5444 };
5445 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5446 }
5447
5448 #[test]
5449 fn supports_answers_per_gesture_where_authorable_cannot() {
5450 assert!(Format::Html.is_authorable());
5455 assert!(Format::Html.supports(Gesture::ToggleInline(InlineKind::Strong)));
5456 assert!(!Format::Html.supports(Gesture::SetBlock));
5457 assert!(!Format::Html.supports(Gesture::ToggleBlockContainer(
5458 BlockContainerKind::BlockQuote
5459 )));
5460 assert!(!Format::Html.supports(Gesture::ToggleCodeBlock));
5461 assert!(!Format::Html.supports(Gesture::InsertLiteral));
5462 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5466 assert!(!Format::Html.supports(Gesture::TableSetAlignment));
5467 assert!(!Format::Html.supports(Gesture::SplitBlock));
5468 assert!(!Format::Html.supports(Gesture::RenumberOrderedLists));
5469 assert!(Format::Markdown.supports(Gesture::TableInsertRow));
5470 assert!(Format::Djot.supports(Gesture::SplitBlock));
5471
5472 for fmt in [Format::Xml] {
5475 assert!(!fmt.is_authorable());
5476 for g in all_gestures() {
5477 assert!(!fmt.supports(g), "{fmt:?} claims to spell {g:?}");
5478 }
5479 }
5480 assert!(Format::Asciidoc.is_authorable());
5483 assert!(Format::Asciidoc.supports(Gesture::SetBlock));
5484 assert!(Format::Asciidoc.supports(Gesture::ToggleInline(InlineKind::Mark)));
5485 assert!(!Format::Asciidoc.supports(Gesture::InsertLink));
5486 assert!(!Format::Asciidoc.supports(Gesture::TableInsertRow));
5487
5488 assert!(Format::Djot.supports(Gesture::ToggleInline(InlineKind::Mark)));
5491 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
5492 assert!(Format::Markdown.supports(Gesture::InsertLineBreak));
5493 assert!(!Format::Djot.supports(Gesture::InsertLineBreak));
5494 }
5495
5496 #[test]
5497 fn supports_agrees_with_what_the_editor_then_does() {
5498 for fmt in [Format::Djot, Format::Markdown, Format::Html] {
5503 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5504 let claimed = fmt.supports(Gesture::ToggleInline(InlineKind::Mark));
5505 let observed = ed.toggle_inline(0, 2, InlineKind::Mark);
5506 assert_eq!(
5507 claimed,
5508 !matches!(observed, Err(Error::UnsupportedFormat)),
5509 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5510 );
5511
5512 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
5513 let claimed = fmt.supports(Gesture::SetBlock);
5514 let observed = ed.set_block(0, BlockKind::Heading(1));
5515 assert_eq!(
5516 claimed,
5517 !matches!(observed, Err(Error::UnsupportedFormat)),
5518 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
5519 );
5520 }
5521
5522 let src = "<table><tr><td>a</td></tr></table>";
5526 let mut ed = Editor::new_str(src, Format::Html).expect("editor");
5527 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5528 assert_eq!(ed.table_insert_row(15, true), Err(Error::UnsupportedFormat));
5529 assert_eq!(ed.renumber_ordered_lists(15), Err(Error::UnsupportedFormat));
5530 assert!(matches!(ed.split_block(15), Err(Error::UnsupportedFormat)));
5531 assert_eq!(ed.source().expect("source"), src.as_bytes());
5532 }
5533
5534 #[test]
5535 fn supports_rides_the_gestures_own_kind_space() {
5536 let (g, k) = Gesture::ToggleBlockContainer(BlockContainerKind::BulletList).to_c();
5541 assert_eq!((g, k), (3, 1));
5542 let (g, k) = Gesture::ToggleInline(InlineKind::Emph).to_c();
5543 assert_eq!((g, k), (1, 1));
5544 assert_eq!(Gesture::InsertLink.to_c(), (10, 0));
5547
5548 let mut out: c_int = 0;
5550 let status = unsafe {
5551 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 10, 3, &mut out)
5552 };
5553 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5554 let status = unsafe {
5555 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 9999, 0, &mut out)
5556 };
5557 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5558 }
5559}