1mod error;
2
3pub(crate) use twig_sys as ffi;
8
9use std::marker::PhantomData;
10use std::ops::Range;
11use std::os::raw::{c_char, c_int};
12use std::ptr::NonNull;
13
14pub use error::Error;
15pub use ffi::TwigSpan as Span;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25#[non_exhaustive]
26pub enum Format {
27 Djot,
28 Markdown,
29 Xml,
30 Html,
31 Asciidoc,
42 Commonmark,
51 Gfm,
57}
58
59impl From<Format> for ffi::TwigFormat {
60 fn from(value: Format) -> Self {
61 match value {
62 Format::Djot => ffi::TwigFormat::Djot,
63 Format::Markdown => ffi::TwigFormat::Markdown,
64 Format::Xml => ffi::TwigFormat::Xml,
65 Format::Html => ffi::TwigFormat::Html,
66 Format::Asciidoc => ffi::TwigFormat::Asciidoc,
67 Format::Commonmark => ffi::TwigFormat::Commonmark,
68 Format::Gfm => ffi::TwigFormat::Gfm,
69 }
70 }
71}
72
73impl Format {
74 pub fn dialect_of(self) -> Option<Format> {
81 match self {
82 Format::Commonmark | Format::Gfm => Some(Format::Markdown),
83 _ => None,
84 }
85 }
86}
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103#[non_exhaustive]
104pub enum Target {
105 Djot,
106 Markdown,
107 Xml,
108 Html,
109 Asciidoc,
113}
114
115impl Target {
116 pub fn as_format(self) -> Option<Format> {
123 match self {
124 Target::Djot => Some(Format::Djot),
125 Target::Markdown => Some(Format::Markdown),
126 Target::Xml => Some(Format::Xml),
127 Target::Html => Some(Format::Html),
128 Target::Asciidoc => Some(Format::Asciidoc),
129 }
130 }
131}
132
133impl From<Format> for Target {
139 fn from(value: Format) -> Self {
140 match value {
141 Format::Djot => Target::Djot,
142 Format::Markdown | Format::Commonmark | Format::Gfm => Target::Markdown,
143 Format::Xml => Target::Xml,
144 Format::Html => Target::Html,
145 Format::Asciidoc => Target::Asciidoc,
146 }
147 }
148}
149
150impl From<Target> for ffi::TwigFormat {
151 fn from(value: Target) -> Self {
152 match value {
153 Target::Djot => ffi::TwigFormat::Djot,
154 Target::Markdown => ffi::TwigFormat::Markdown,
155 Target::Xml => ffi::TwigFormat::Xml,
156 Target::Html => ffi::TwigFormat::Html,
157 Target::Asciidoc => ffi::TwigFormat::Asciidoc,
158 }
159 }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq, Hash)]
196#[non_exhaustive]
197pub enum Kind {
198 Doc,
200 Para,
202 Heading,
203 ThematicBreak,
204 Section,
205 CodeBlock,
206 RawBlock,
207 Metadata,
208 BlockQuote,
209 BulletList,
210 OrderedList,
211 TaskList,
212 DefinitionList,
213 LineBlock,
214 Table,
215 ListItem,
217 TaskListItem,
218 DefinitionListItem,
219 Term,
220 Definition,
221 Line,
222 Row,
223 Cell,
224 Column,
225 Caption,
226 Footnote,
227 Reference,
228 Citation,
229 Substitution,
230 Str,
232 SoftBreak,
233 HardBreak,
234 NonBreakingSpace,
235 RawInline,
236 SmartPunctuation,
237 Link,
238 Image,
239 Emph,
241 Strong,
242 Mark,
243 Superscript,
244 Subscript,
245 Insert,
246 Delete,
247 DoubleQuoted,
248 SingleQuoted,
249 Symb,
251 Verbatim,
252 InlineMath,
253 DisplayMath,
254 Url,
255 Email,
256 FootnoteReference,
257 CitationReference,
258 SubstitutionReference,
259 Container,
261 ProcessingInstruction,
262 Comment,
263 Doctype,
264 Cdata,
265 Other(String),
272}
273
274impl Kind {
275 pub fn as_str(&self) -> &str {
278 match self {
279 Kind::Doc => "doc",
280 Kind::Para => "para",
281 Kind::Heading => "heading",
282 Kind::ThematicBreak => "thematic_break",
283 Kind::Section => "section",
284 Kind::CodeBlock => "code_block",
285 Kind::RawBlock => "raw_block",
286 Kind::Metadata => "metadata",
287 Kind::BlockQuote => "block_quote",
288 Kind::BulletList => "bullet_list",
289 Kind::OrderedList => "ordered_list",
290 Kind::TaskList => "task_list",
291 Kind::DefinitionList => "definition_list",
292 Kind::LineBlock => "line_block",
293 Kind::Table => "table",
294 Kind::ListItem => "list_item",
295 Kind::TaskListItem => "task_list_item",
296 Kind::DefinitionListItem => "definition_list_item",
297 Kind::Term => "term",
298 Kind::Definition => "definition",
299 Kind::Line => "line",
300 Kind::Row => "row",
301 Kind::Cell => "cell",
302 Kind::Column => "column",
303 Kind::Caption => "caption",
304 Kind::Footnote => "footnote",
305 Kind::Reference => "reference",
306 Kind::Citation => "citation",
307 Kind::Substitution => "substitution",
308 Kind::Str => "str",
309 Kind::SoftBreak => "soft_break",
310 Kind::HardBreak => "hard_break",
311 Kind::NonBreakingSpace => "non_breaking_space",
312 Kind::RawInline => "raw_inline",
313 Kind::SmartPunctuation => "smart_punctuation",
314 Kind::Link => "link",
315 Kind::Image => "image",
316 Kind::Container => "container",
317 Kind::ProcessingInstruction => "processing_instruction",
318 Kind::Emph => "emph",
319 Kind::Strong => "strong",
320 Kind::Mark => "mark",
321 Kind::Superscript => "superscript",
322 Kind::Subscript => "subscript",
323 Kind::Insert => "insert",
324 Kind::Delete => "delete",
325 Kind::DoubleQuoted => "double_quoted",
326 Kind::SingleQuoted => "single_quoted",
327 Kind::Symb => "symb",
328 Kind::Verbatim => "verbatim",
329 Kind::InlineMath => "inline_math",
330 Kind::DisplayMath => "display_math",
331 Kind::Url => "url",
332 Kind::Email => "email",
333 Kind::FootnoteReference => "footnote_reference",
334 Kind::CitationReference => "citation_reference",
335 Kind::SubstitutionReference => "substitution_reference",
336 Kind::Comment => "comment",
337 Kind::Doctype => "doctype",
338 Kind::Cdata => "cdata",
339 Kind::Other(name) => name.as_str(),
340 }
341 }
342
343 pub fn is_unknown(&self) -> bool {
347 matches!(self, Kind::Other(_))
348 }
349}
350
351impl From<&str> for Kind {
352 fn from(name: &str) -> Self {
353 match name {
354 "doc" => Kind::Doc,
355 "para" => Kind::Para,
356 "heading" => Kind::Heading,
357 "thematic_break" => Kind::ThematicBreak,
358 "section" => Kind::Section,
359 "code_block" => Kind::CodeBlock,
360 "raw_block" => Kind::RawBlock,
361 "metadata" => Kind::Metadata,
362 "block_quote" => Kind::BlockQuote,
363 "bullet_list" => Kind::BulletList,
364 "ordered_list" => Kind::OrderedList,
365 "task_list" => Kind::TaskList,
366 "definition_list" => Kind::DefinitionList,
367 "line_block" => Kind::LineBlock,
368 "table" => Kind::Table,
369 "list_item" => Kind::ListItem,
370 "task_list_item" => Kind::TaskListItem,
371 "definition_list_item" => Kind::DefinitionListItem,
372 "term" => Kind::Term,
373 "definition" => Kind::Definition,
374 "line" => Kind::Line,
375 "row" => Kind::Row,
376 "cell" => Kind::Cell,
377 "column" => Kind::Column,
378 "caption" => Kind::Caption,
379 "footnote" => Kind::Footnote,
380 "reference" => Kind::Reference,
381 "citation" => Kind::Citation,
382 "substitution" => Kind::Substitution,
383 "str" => Kind::Str,
384 "soft_break" => Kind::SoftBreak,
385 "hard_break" => Kind::HardBreak,
386 "non_breaking_space" => Kind::NonBreakingSpace,
387 "raw_inline" => Kind::RawInline,
388 "smart_punctuation" => Kind::SmartPunctuation,
389 "link" => Kind::Link,
390 "image" => Kind::Image,
391 "container" => Kind::Container,
392 "processing_instruction" => Kind::ProcessingInstruction,
393 "emph" => Kind::Emph,
394 "strong" => Kind::Strong,
395 "mark" => Kind::Mark,
396 "superscript" => Kind::Superscript,
397 "subscript" => Kind::Subscript,
398 "insert" => Kind::Insert,
399 "delete" => Kind::Delete,
400 "double_quoted" => Kind::DoubleQuoted,
401 "single_quoted" => Kind::SingleQuoted,
402 "symb" => Kind::Symb,
403 "verbatim" => Kind::Verbatim,
404 "inline_math" => Kind::InlineMath,
405 "display_math" => Kind::DisplayMath,
406 "url" => Kind::Url,
407 "email" => Kind::Email,
408 "footnote_reference" => Kind::FootnoteReference,
409 "citation_reference" => Kind::CitationReference,
410 "substitution_reference" => Kind::SubstitutionReference,
411 "comment" => Kind::Comment,
412 "doctype" => Kind::Doctype,
413 "cdata" => Kind::Cdata,
414 other => Kind::Other(other.to_string()),
415 }
416 }
417}
418
419impl std::fmt::Display for Kind {
420 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421 f.write_str(self.as_str())
422 }
423}
424
425#[derive(Clone, Debug, Eq, PartialEq)]
427pub struct QueryMatch {
428 pub node_id: u32,
430 pub span: Range<usize>,
432 pub content_span: Option<Range<usize>>,
435 pub kind: Kind,
438}
439
440#[derive(Clone, Debug, Eq, PartialEq)]
447pub struct Change {
448 pub old: Range<usize>,
449 pub new: Range<usize>,
450}
451
452impl Change {
453 pub fn delta(&self) -> isize {
455 self.new.len() as isize - self.old.len() as isize
456 }
457
458 fn from_ffi(c: ffi::TwigChange) -> Self {
459 Change {
460 old: c.old_span.start..c.old_span.end,
461 new: c.new_span.start..c.new_span.end,
462 }
463 }
464}
465
466#[derive(Clone, Debug, Eq, PartialEq)]
477#[non_exhaustive]
478pub struct FlatNode {
479 pub id: NodeId,
480 pub parent: Option<NodeId>,
481 pub first_child: Option<NodeId>,
482 pub next_sibling: Option<NodeId>,
483 pub span: Range<usize>,
484 pub content_span: Option<Range<usize>>,
485 pub level: Option<u32>,
487 pub kind: Kind,
488 pub text: Option<String>,
496 pub destination: Option<String>,
497 pub head: Option<bool>,
500 pub alignment: Option<Alignment>,
506 pub name: Option<String>,
518 pub directive_form: Option<DirectiveForm>,
533 pub origin: Option<ContainerOrigin>,
543 pub marker_span: Option<Range<usize>>,
562 pub checked: Option<bool>,
575 pub attrs: Vec<(String, Option<String>)>,
579}
580
581#[derive(Clone, Debug, Default, Eq, PartialEq)]
589pub struct LinePrefix {
590 pub text: String,
592 pub columns: usize,
594}
595
596#[derive(Clone, Copy, Debug, Eq, PartialEq)]
606pub enum InlineKind {
607 Strong,
608 Emph,
609 Verbatim,
610 Mark,
611 Superscript,
612 Subscript,
613 Insert,
614 Delete,
615}
616
617impl InlineKind {
618 fn to_c(self) -> c_int {
619 match self {
620 InlineKind::Strong => 0,
621 InlineKind::Emph => 1,
622 InlineKind::Verbatim => 2,
623 InlineKind::Mark => 3,
624 InlineKind::Superscript => 4,
625 InlineKind::Subscript => 5,
626 InlineKind::Insert => 6,
627 InlineKind::Delete => 7,
628 }
629 }
630}
631
632#[derive(Clone, Copy, Debug, Eq, PartialEq)]
634pub enum BlockKind {
635 Paragraph,
636 Heading(u32),
638}
639
640impl BlockKind {
641 fn to_c(self) -> (c_int, u32) {
643 match self {
644 BlockKind::Paragraph => (0, 0),
645 BlockKind::Heading(level) => (1, level),
646 }
647 }
648}
649
650#[derive(Clone, Copy, Debug, Eq, PartialEq)]
656pub enum BlockContainerKind {
657 BlockQuote,
658 BulletList,
659 OrderedList,
660}
661
662impl BlockContainerKind {
663 fn to_c(self) -> c_int {
664 match self {
665 BlockContainerKind::BlockQuote => 0,
666 BlockContainerKind::BulletList => 1,
667 BlockContainerKind::OrderedList => 2,
668 }
669 }
670}
671
672#[derive(Clone, Copy, Debug, Eq, PartialEq)]
686pub enum MarkColor {
687 Red,
688 Orange,
689 Yellow,
690 Green,
691 Blue,
692 Purple,
693 Brown,
694}
695
696impl MarkColor {
697 pub fn as_str(self) -> &'static str {
699 match self {
700 MarkColor::Red => "red",
701 MarkColor::Orange => "orange",
702 MarkColor::Yellow => "yellow",
703 MarkColor::Green => "green",
704 MarkColor::Blue => "blue",
705 MarkColor::Purple => "purple",
706 MarkColor::Brown => "brown",
707 }
708 }
709
710 pub fn from_str(s: &str) -> Option<Self> {
713 Some(match s {
714 "red" => MarkColor::Red,
715 "orange" => MarkColor::Orange,
716 "yellow" => MarkColor::Yellow,
717 "green" => MarkColor::Green,
718 "blue" => MarkColor::Blue,
719 "purple" => MarkColor::Purple,
720 "brown" => MarkColor::Brown,
721 _ => return None,
722 })
723 }
724}
725
726#[derive(Clone, Copy, Debug, Eq, PartialEq)]
767#[non_exhaustive]
768pub enum Gesture {
769 WrapRange(InlineKind),
770 ToggleInline(InlineKind),
771 SetBlock,
772 ToggleBlockContainer(BlockContainerKind),
773 InsertThematicBreak,
774 ToggleCodeBlock,
775 SetCodeLanguage,
776 ToggleTaskItem,
777 SetTaskChecked,
778 ToggleTaskChecked,
779 InsertLink,
780 InsertImage,
781 InsertFootnote,
782 InsertLiteral,
783 InsertLineBreak,
784 SplitBlock,
785 RenumberOrderedLists,
786 TableInsertRow,
787 TableDeleteRow,
788 TableInsertColumn,
789 TableDeleteColumn,
790 TableSetAlignment,
791 TableMoveRow,
792 TableMoveColumn,
793 SetMarkColor,
801 InsertTable,
804}
805
806impl Gesture {
807 fn to_c(self) -> (c_int, c_int) {
812 match self {
813 Gesture::WrapRange(k) => (0, k.to_c()),
814 Gesture::ToggleInline(k) => (1, k.to_c()),
815 Gesture::SetBlock => (2, 0),
816 Gesture::ToggleBlockContainer(k) => (3, k.to_c()),
817 Gesture::InsertThematicBreak => (4, 0),
818 Gesture::ToggleCodeBlock => (5, 0),
819 Gesture::SetCodeLanguage => (6, 0),
820 Gesture::ToggleTaskItem => (7, 0),
821 Gesture::SetTaskChecked => (8, 0),
822 Gesture::ToggleTaskChecked => (9, 0),
823 Gesture::InsertLink => (10, 0),
824 Gesture::InsertImage => (11, 0),
825 Gesture::InsertFootnote => (12, 0),
826 Gesture::InsertLiteral => (13, 0),
827 Gesture::InsertLineBreak => (14, 0),
828 Gesture::SplitBlock => (15, 0),
829 Gesture::RenumberOrderedLists => (16, 0),
830 Gesture::TableInsertRow => (17, 0),
831 Gesture::TableDeleteRow => (18, 0),
832 Gesture::TableInsertColumn => (19, 0),
833 Gesture::TableDeleteColumn => (20, 0),
834 Gesture::TableSetAlignment => (21, 0),
835 Gesture::TableMoveRow => (22, 0),
836 Gesture::TableMoveColumn => (23, 0),
837 Gesture::SetMarkColor => (24, 0),
838 Gesture::InsertTable => (25, 0),
839 }
840 }
841}
842
843impl Format {
844 pub fn supports(self, gesture: Gesture) -> bool {
869 let (g, k) = gesture.to_c();
870 let mut supported: c_int = 0;
871 let status = unsafe {
872 ffi::twig_format_supports(ffi::TwigFormat::from(self) as c_int, g, k, &mut supported)
873 };
874 debug_assert!(
875 Error::from_status(status).is_ok(),
876 "twig_format_supports rejected a combination the Rust types make unrepresentable",
877 );
878 supported == 1
879 }
880
881 pub fn supports_with(self, extensions: MarkdownExtensions, gesture: Gesture) -> bool {
905 let (g, k) = gesture.to_c();
906 let mut supported: c_int = 0;
907 let status = unsafe {
908 ffi::twig_format_supports_ext(
909 ffi::TwigFormat::from(self) as c_int,
910 extensions.to_flags(),
911 g,
912 k,
913 &mut supported,
914 )
915 };
916 debug_assert!(
917 Error::from_status(status).is_ok(),
918 "twig_format_supports_ext rejected a combination the Rust types make unrepresentable",
919 );
920 supported == 1
921 }
922
923 pub fn is_authorable(self) -> bool {
933 let mut authorable: c_int = 0;
934 let status = unsafe {
935 ffi::twig_format_is_authorable(ffi::TwigFormat::from(self) as c_int, &mut authorable)
936 };
937 debug_assert!(Error::from_status(status).is_ok(), "unknown format code");
938 authorable == 1
939 }
940}
941
942#[derive(Clone, Copy, Debug, Eq, PartialEq)]
943pub struct Version {
944 pub major: u8,
945 pub minor: u8,
946 pub patch: u8,
947}
948
949pub fn version() -> Version {
950 let packed = unsafe { ffi::twig_version() };
951 Version {
952 major: (packed >> 16) as u8,
953 minor: (packed >> 8) as u8,
954 patch: packed as u8,
955 }
956}
957
958pub const ABI_VERSION: u32 = ffi::TWIG_ABI_VERSION;
964
965pub fn abi_version() -> u32 {
971 unsafe { ffi::twig_abi_version() }
972}
973
974pub fn version_string() -> &'static str {
975 let ptr = unsafe { ffi::twig_version_string() };
976 unsafe { std::ffi::CStr::from_ptr(ptr) }
977 .to_str()
978 .unwrap_or("")
979}
980
981#[derive(Debug)]
982pub struct Document {
983 raw: NonNull<ffi::TwigDocument>,
984}
985
986impl Document {
987 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
988 Self::parse_with(input, format, MarkdownExtensions::default())
989 }
990
991 pub fn parse_str(input: &str, format: Format) -> Result<Self, Error> {
992 Self::parse(input.as_bytes(), format)
993 }
994
995 pub fn parse_with(
1001 input: &[u8],
1002 format: Format,
1003 extensions: MarkdownExtensions,
1004 ) -> Result<Self, Error> {
1005 let mut raw = std::ptr::null_mut();
1006 let ffi_format: ffi::TwigFormat = format.into();
1007 let status = unsafe {
1008 ffi::twig_parse_ext(
1009 input.as_ptr(),
1010 input.len(),
1011 ffi_format as i32,
1012 extensions.to_flags(),
1013 &mut raw,
1014 )
1015 };
1016 Error::from_status(status)?;
1017 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1018 Ok(Self { raw })
1019 }
1020
1021 pub fn parse_str_with(
1023 input: &str,
1024 format: Format,
1025 extensions: MarkdownExtensions,
1026 ) -> Result<Self, Error> {
1027 Self::parse_with(input.as_bytes(), format, extensions)
1028 }
1029
1030 pub fn render_html(&mut self) -> Result<Vec<u8>, Error> {
1033 let raw = self.raw.as_ptr();
1034 collect_bytes(|ptr, len| unsafe { ffi::twig_document_render_html(raw, ptr, len) })
1035 }
1036
1037 pub fn serialize_to(&mut self, target: Target) -> Result<Vec<u8>, Error> {
1048 let raw = self.raw.as_ptr();
1049 let ffi_target: ffi::TwigFormat = target.into();
1050 collect_bytes(|ptr, len| unsafe {
1051 ffi::twig_document_serialize(raw, ffi_target as i32, ptr, len)
1052 })
1053 }
1054
1055 pub fn serialize(&mut self, format: Format) -> Result<Vec<u8>, Error> {
1062 self.serialize_to(format.into())
1063 }
1064
1065 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
1068 let raw = self.raw.as_ptr();
1069 collect_bytes(|ptr, len| unsafe { ffi::twig_document_ast_json(raw, ptr, len) })
1070 }
1071
1072 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1081 let raw = self.raw.as_ptr();
1082 collect_matches(|ptr, len| unsafe {
1083 ffi::twig_document_query(raw, selector.as_ptr(), selector.len(), ptr, len)
1084 })
1085 }
1086
1087 pub fn span(&mut self, node: NodeId) -> Result<Range<usize>, Error> {
1089 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1090 let status = unsafe { ffi::twig_document_node_span(self.raw.as_ptr(), node.0, &mut span) };
1091 Error::from_status(status)?;
1092 Ok(span.start..span.end)
1093 }
1094
1095 pub fn content_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
1098 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1099 let status =
1100 unsafe { ffi::twig_document_node_content_span(self.raw.as_ptr(), node.0, &mut span) };
1101 match status.0 {
1102 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1103 ffi::TwigStatus::NOT_FOUND => Ok(None),
1104 _ => Err(Error::from_status(status).unwrap_err()),
1105 }
1106 }
1107
1108 pub fn marker_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
1112 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1113 let status =
1114 unsafe { ffi::twig_document_node_marker_span(self.raw.as_ptr(), node.0, &mut span) };
1115 match status.0 {
1116 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1117 ffi::TwigStatus::NOT_FOUND => Ok(None),
1118 _ => Err(Error::from_status(status).unwrap_err()),
1119 }
1120 }
1121
1122 pub fn attrs_span(&mut self, node: NodeId) -> Result<Option<Range<usize>>, Error> {
1137 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1138 let status =
1139 unsafe { ffi::twig_document_attrs_span(self.raw.as_ptr(), node.0, &mut span) };
1140 match status.0 {
1141 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1142 ffi::TwigStatus::NOT_FOUND => Ok(None),
1143 _ => Err(Error::from_status(status).unwrap_err()),
1144 }
1145 }
1146
1147 pub fn line_prefix(&mut self, offset: usize) -> Result<Option<Range<usize>>, Error> {
1164 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1165 let status =
1166 unsafe { ffi::twig_document_line_prefix(self.raw.as_ptr(), offset, &mut span) };
1167 match status.0 {
1168 ffi::TwigStatus::OK => Ok(Some(span.start..span.end)),
1169 ffi::TwigStatus::NOT_FOUND => Ok(None),
1170 _ => Err(Error::from_status(status).unwrap_err()),
1171 }
1172 }
1173
1174 pub fn continuation_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1200 self.prefix_via(offset, ffi::twig_document_continuation_prefix)
1201 }
1202
1203 pub fn blank_line_prefix(&mut self, offset: usize) -> Result<LinePrefix, Error> {
1215 self.prefix_via(offset, ffi::twig_document_blank_line_prefix)
1216 }
1217
1218 fn prefix_via(
1220 &mut self,
1221 offset: usize,
1222 f: unsafe extern "C" fn(
1223 *mut ffi::TwigDocument,
1224 usize,
1225 *mut *const u8,
1226 *mut usize,
1227 *mut usize,
1228 ) -> ffi::TwigStatus,
1229 ) -> Result<LinePrefix, Error> {
1230 let mut ptr: *const u8 = std::ptr::null();
1231 let mut len = 0usize;
1232 let mut columns = 0usize;
1233 let status = unsafe { f(self.raw.as_ptr(), offset, &mut ptr, &mut len, &mut columns) };
1234 Error::from_status(status)?;
1235 let text = if ptr.is_null() || len == 0 {
1236 String::new()
1237 } else {
1238 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
1239 String::from_utf8(bytes.to_vec()).map_err(|_| Error::Internal)?
1240 };
1241 Ok(LinePrefix { text, columns })
1242 }
1243
1244 pub fn cell_extent(&mut self, node: NodeId) -> Result<Option<(u32, u32)>, Error> {
1256 let raw = self.raw.as_ptr();
1257 let mut colspan: u32 = 0;
1258 let status = unsafe { ffi::twig_document_cell_colspan(raw, node.0, &mut colspan) };
1259 match status.0 {
1260 ffi::TwigStatus::OK => {}
1261 ffi::TwigStatus::NOT_FOUND => return Ok(None),
1262 _ => return Err(Error::from_status(status).unwrap_err()),
1263 }
1264 let mut rowspan: u32 = 0;
1265 Error::from_status(unsafe { ffi::twig_document_cell_rowspan(raw, node.0, &mut rowspan) })?;
1266 Ok(Some((colspan, rowspan)))
1267 }
1268
1269 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1274 let raw = self.raw.as_ptr();
1275 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_nodes(raw, ptr, len) })
1276 }
1277
1278 pub fn definitions(&mut self) -> Result<Vec<QueryMatch>, Error> {
1295 let raw = self.raw.as_ptr();
1296 collect_matches(|ptr, len| unsafe { ffi::twig_document_definitions(raw, ptr, len) })
1297 }
1298
1299 pub fn diagnostics(&mut self, target: Target) -> Result<Vec<Warning>, Error> {
1319 let raw = self.raw.as_ptr();
1320 let code = ffi::TwigFormat::from(target) as c_int;
1321 let mut ptr: *const ffi::TwigWarning = std::ptr::null();
1322 let mut len = 0usize;
1323 let status = unsafe { ffi::twig_document_diagnostics(raw, code, &mut ptr, &mut len) };
1324 Error::from_status(status)?;
1325 if len == 0 || ptr.is_null() {
1326 return Ok(Vec::new());
1327 }
1328 let raw_warnings = unsafe { std::slice::from_raw_parts(ptr, len) };
1329 Ok(raw_warnings
1330 .iter()
1331 .map(|w| Warning {
1332 fidelity: Fidelity::from_c(w.fidelity),
1333 path: borrowed_bytes(w.path_ptr, w.path_len).unwrap_or_default(),
1334 kind: Kind::from(borrowed_cstr(w.kind).unwrap_or_default().as_str()),
1335 })
1336 .collect())
1337 }
1338
1339 pub fn children(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1345 let raw = self.raw.as_ptr();
1346 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1347 collect_matches(|ptr, len| unsafe { ffi::twig_document_children(raw, id, ptr, len) })
1348 }
1349
1350 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1356 let raw = self.raw.as_ptr();
1357 collect_flat_nodes(|ptr, len| unsafe { ffi::twig_document_subtree(raw, node.0, ptr, len) })
1358 }
1359
1360 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1365 let mut m = empty_ffi_match();
1366 let status = unsafe { ffi::twig_document_node_at(self.raw.as_ptr(), offset, &mut m) };
1367 match status.0 {
1368 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1369 ffi::TwigStatus::NOT_FOUND => Ok(None),
1370 _ => Err(Error::from_status(status).unwrap_err()),
1371 }
1372 }
1373
1374 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1378 let raw = self.raw.as_ptr();
1379 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1380 let mut len = 0usize;
1381 let status = unsafe { ffi::twig_document_nodes_at(raw, offset, &mut ptr, &mut len) };
1382 match status.0 {
1383 ffi::TwigStatus::OK => {}
1384 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1385 _ => return Err(Error::from_status(status).unwrap_err()),
1386 }
1387 if len == 0 || ptr.is_null() {
1388 return Ok(Vec::new());
1389 }
1390 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1391 raw_matches.iter().map(query_match_from_ffi).collect()
1392 }
1393
1394 pub fn node_at_caret(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1417 let mut m = empty_ffi_match();
1418 let status = unsafe { ffi::twig_document_node_at_caret(self.raw.as_ptr(), offset, &mut m) };
1419 match status.0 {
1420 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1421 ffi::TwigStatus::NOT_FOUND => Ok(None),
1422 _ => Err(Error::from_status(status).unwrap_err()),
1423 }
1424 }
1425
1426 pub fn ancestors_at_caret(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1430 let raw = self.raw.as_ptr();
1431 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1432 let mut len = 0usize;
1433 let status = unsafe { ffi::twig_document_nodes_at_caret(raw, offset, &mut ptr, &mut len) };
1434 match status.0 {
1435 ffi::TwigStatus::OK => {}
1436 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1437 _ => return Err(Error::from_status(status).unwrap_err()),
1438 }
1439 if len == 0 || ptr.is_null() {
1440 return Ok(Vec::new());
1441 }
1442 let raw_matches = unsafe { std::slice::from_raw_parts(ptr, len) };
1443 raw_matches.iter().map(query_match_from_ffi).collect()
1444 }
1445}
1446
1447#[derive(Debug)]
1458pub struct DocumentView<'a> {
1459 doc: Document,
1460 _editor: PhantomData<&'a mut Editor>,
1461}
1462
1463impl std::ops::Deref for DocumentView<'_> {
1464 type Target = Document;
1465
1466 fn deref(&self) -> &Document {
1467 &self.doc
1468 }
1469}
1470
1471impl std::ops::DerefMut for DocumentView<'_> {
1472 fn deref_mut(&mut self) -> &mut Document {
1473 &mut self.doc
1474 }
1475}
1476
1477impl Drop for Document {
1478 fn drop(&mut self) {
1479 unsafe { ffi::twig_document_destroy(self.raw.as_ptr()) }
1480 }
1481}
1482
1483#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1493pub struct MarkdownExtensions {
1494 pub directives: bool,
1496 pub math: bool,
1498 pub html_elements: bool,
1503 pub highlight: bool,
1506 pub highlight_colors: bool,
1512}
1513
1514impl MarkdownExtensions {
1515 fn to_flags(self) -> u32 {
1516 let mut flags = 0;
1517 if self.directives {
1518 flags |= ffi::TWIG_MD_DIRECTIVES;
1519 }
1520 if self.math {
1521 flags |= ffi::TWIG_MD_MATH;
1522 }
1523 if self.html_elements {
1524 flags |= ffi::TWIG_MD_HTML_ELEMENTS;
1525 }
1526 if self.highlight {
1527 flags |= ffi::TWIG_MD_HIGHLIGHT;
1528 }
1529 if self.highlight_colors {
1530 flags |= ffi::TWIG_MD_HIGHLIGHT_COLORS;
1531 }
1532 flags
1533 }
1534}
1535
1536#[derive(Debug)]
1542pub struct Editor {
1543 raw: NonNull<ffi::TwigEditor>,
1544}
1545
1546impl Editor {
1547 pub fn new(input: &[u8], format: Format) -> Result<Self, Error> {
1550 let mut raw = std::ptr::null_mut();
1551 let ffi_format: ffi::TwigFormat = format.into();
1552 let status = unsafe {
1553 ffi::twig_editor_create(input.as_ptr(), input.len(), ffi_format as i32, &mut raw)
1554 };
1555 Error::from_status(status)?;
1556 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1557 Ok(Self { raw })
1558 }
1559
1560 pub fn new_str(input: &str, format: Format) -> Result<Self, Error> {
1561 Self::new(input.as_bytes(), format)
1562 }
1563
1564 pub fn new_ext(
1578 input: &[u8],
1579 format: Format,
1580 extensions: MarkdownExtensions,
1581 ) -> Result<Self, Error> {
1582 let mut raw = std::ptr::null_mut();
1583 let ffi_format: ffi::TwigFormat = format.into();
1584 let status = unsafe {
1585 ffi::twig_editor_create_ext(
1586 input.as_ptr(),
1587 input.len(),
1588 ffi_format as i32,
1589 extensions.to_flags(),
1590 &mut raw,
1591 )
1592 };
1593 Error::from_status(status)?;
1594 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1595 Ok(Self { raw })
1596 }
1597
1598 pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1600 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1601 ffi::twig_editor_replace(ed, loc, loc_len, txt, txt_len)
1602 })
1603 }
1604
1605 pub fn replace_content(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1608 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1609 ffi::twig_editor_replace_content(ed, loc, loc_len, txt, txt_len)
1610 })
1611 }
1612
1613 pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1615 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1616 ffi::twig_editor_insert_before(ed, loc, loc_len, txt, txt_len)
1617 })
1618 }
1619
1620 pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error> {
1622 self.apply(locator, text, |ed, loc, loc_len, txt, txt_len| unsafe {
1623 ffi::twig_editor_insert_after(ed, loc, loc_len, txt, txt_len)
1624 })
1625 }
1626
1627 pub fn insert_child(&mut self, locator: &str, index: usize, text: &str) -> Result<(), Error> {
1630 let status = unsafe {
1631 ffi::twig_editor_insert_child(
1632 self.raw.as_ptr(),
1633 locator.as_ptr(),
1634 locator.len(),
1635 index,
1636 text.as_ptr(),
1637 text.len(),
1638 )
1639 };
1640 Error::from_status(status)
1641 }
1642
1643 pub fn delete(&mut self, locator: &str) -> Result<(), Error> {
1646 let status =
1647 unsafe { ffi::twig_editor_delete(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1648 Error::from_status(status)
1649 }
1650
1651 pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error> {
1654 let status = unsafe {
1655 ffi::twig_editor_delete_smart(self.raw.as_ptr(), locator.as_ptr(), locator.len())
1656 };
1657 Error::from_status(status)
1658 }
1659
1660 pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error> {
1664 let status =
1665 unsafe { ffi::twig_editor_unwrap(self.raw.as_ptr(), locator.as_ptr(), locator.len()) };
1666 Error::from_status(status)
1667 }
1668
1669 pub fn filter(
1674 &mut self,
1675 drop: &str,
1676 keep: Option<&str>,
1677 unwrap_kept: bool,
1678 ) -> Result<(), Error> {
1679 let (keep_ptr, keep_len) = match keep {
1680 Some(k) => (k.as_ptr(), k.len()),
1681 None => (std::ptr::null(), 0),
1682 };
1683 let status = unsafe {
1684 ffi::twig_editor_filter(
1685 self.raw.as_ptr(),
1686 drop.as_ptr(),
1687 drop.len(),
1688 keep_ptr,
1689 keep_len,
1690 unwrap_kept as i32,
1691 )
1692 };
1693 Error::from_status(status)
1694 }
1695
1696 pub fn source(&mut self) -> Result<Vec<u8>, Error> {
1698 let raw = self.raw.as_ptr();
1699 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_source(raw, ptr, len) })
1700 }
1701
1702 pub fn source_str(&mut self) -> Result<String, Error> {
1704 String::from_utf8(self.source()?).map_err(|_| Error::Internal)
1705 }
1706
1707 pub fn ast_json(&mut self) -> Result<Vec<u8>, Error> {
1710 let raw = self.raw.as_ptr();
1711 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_ast_json(raw, ptr, len) })
1712 }
1713
1714 pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error> {
1717 let raw = self.raw.as_ptr();
1718 collect_matches(|ptr, len| unsafe {
1719 ffi::twig_editor_query(raw, selector.as_ptr(), selector.len(), ptr, len)
1720 })
1721 }
1722
1723 pub fn edit_range(&mut self, start: usize, end: usize, text: &str) -> Result<Change, Error> {
1733 let mut change = ffi::TwigChange {
1734 old_span: ffi::TwigSpan { start: 0, end: 0 },
1735 new_span: ffi::TwigSpan { start: 0, end: 0 },
1736 };
1737 let status = unsafe {
1738 ffi::twig_editor_edit_range(
1739 self.raw.as_ptr(),
1740 start,
1741 end,
1742 text.as_ptr(),
1743 text.len(),
1744 &mut change,
1745 )
1746 };
1747 Error::from_status(status)?;
1748 Ok(Change::from_ffi(change))
1749 }
1750
1751 pub fn last_change(&mut self) -> Option<Change> {
1757 let mut change = ffi::TwigChange {
1758 old_span: ffi::TwigSpan { start: 0, end: 0 },
1759 new_span: ffi::TwigSpan { start: 0, end: 0 },
1760 };
1761 let status = unsafe { ffi::twig_editor_last_change(self.raw.as_ptr(), &mut change) };
1762 match status.0 {
1763 ffi::TwigStatus::OK => Some(Change::from_ffi(change)),
1764 _ => None,
1765 }
1766 }
1767
1768 pub fn undo(&mut self) -> Result<Option<Change>, Error> {
1773 let mut change = ffi::TwigChange {
1774 old_span: ffi::TwigSpan { start: 0, end: 0 },
1775 new_span: ffi::TwigSpan { start: 0, end: 0 },
1776 };
1777 let status = unsafe { ffi::twig_editor_undo(self.raw.as_ptr(), &mut change) };
1778 if status.0 == ffi::TwigStatus::NOT_FOUND {
1779 return Ok(None);
1780 }
1781 Error::from_status(status)?;
1782 Ok(Some(Change::from_ffi(change)))
1783 }
1784
1785 pub fn redo(&mut self) -> Result<Option<Change>, Error> {
1789 let mut change = ffi::TwigChange {
1790 old_span: ffi::TwigSpan { start: 0, end: 0 },
1791 new_span: ffi::TwigSpan { start: 0, end: 0 },
1792 };
1793 let status = unsafe { ffi::twig_editor_redo(self.raw.as_ptr(), &mut change) };
1794 if status.0 == ffi::TwigStatus::NOT_FOUND {
1795 return Ok(None);
1796 }
1797 Error::from_status(status)?;
1798 Ok(Some(Change::from_ffi(change)))
1799 }
1800
1801 pub fn coalesce_last_undo(&mut self) -> Result<(), Error> {
1806 let status = unsafe { ffi::twig_editor_coalesce_last(self.raw.as_ptr()) };
1807 Error::from_status(status)
1808 }
1809
1810 pub fn revision(&mut self) -> u64 {
1816 unsafe { ffi::twig_editor_revision(self.raw.as_ptr()) }
1817 }
1818
1819 pub fn dirty_range(&mut self) -> Option<Range<usize>> {
1840 let mut span = ffi::TwigSpan { start: 0, end: 0 };
1841 let status = unsafe { ffi::twig_editor_dirty_range(self.raw.as_ptr(), &mut span) };
1842 match status.0 {
1843 ffi::TwigStatus::OK => Some(span.start..span.end),
1844 _ => None,
1845 }
1846 }
1847
1848 pub fn clear_dirty(&mut self) {
1853 unsafe { ffi::twig_editor_clear_dirty(self.raw.as_ptr()) };
1854 }
1855
1856 pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error> {
1864 let status = unsafe {
1865 ffi::twig_editor_set_caret_blob(self.raw.as_ptr(), blob.as_ptr(), blob.len())
1866 };
1867 Error::from_status(status)
1868 }
1869
1870 pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error> {
1875 let raw = self.raw.as_ptr();
1876 collect_bytes(|ptr, len| unsafe { ffi::twig_editor_caret_blob(raw, ptr, len) })
1877 }
1878
1879 pub fn document(&mut self) -> Result<DocumentView<'_>, Error> {
1888 let mut raw = std::ptr::null_mut();
1889 let status = unsafe { ffi::twig_editor_document(self.raw.as_ptr(), &mut raw) };
1890 Error::from_status(status)?;
1891 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
1892 Ok(DocumentView {
1893 doc: Document { raw },
1894 _editor: PhantomData,
1895 })
1896 }
1897
1898 pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error> {
1903 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1904 let mut len = 0usize;
1905 let status = unsafe { ffi::twig_editor_nodes(self.raw.as_ptr(), &mut ptr, &mut len) };
1906 Error::from_status(status)?;
1907 if len == 0 {
1908 return Ok(Vec::new());
1909 }
1910 if ptr.is_null() {
1911 return Err(Error::Internal);
1912 }
1913 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1914 raw.iter().map(flat_node_from_ffi).collect()
1915 }
1916
1917 pub fn child_spans(&mut self, node: Option<NodeId>) -> Result<Vec<QueryMatch>, Error> {
1924 let id = node.map_or(ffi::TWIG_NO_NODE, |n| n.0);
1925 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1926 let mut len = 0usize;
1927 let status =
1928 unsafe { ffi::twig_editor_child_spans(self.raw.as_ptr(), id, &mut ptr, &mut len) };
1929 Error::from_status(status)?;
1930 if len == 0 || ptr.is_null() {
1931 return Ok(Vec::new());
1932 }
1933 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1934 raw.iter().map(query_match_from_ffi).collect()
1935 }
1936
1937 pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error> {
1945 let mut ptr: *const ffi::TwigFlatNode = std::ptr::null();
1946 let mut len = 0usize;
1947 let status =
1948 unsafe { ffi::twig_editor_subtree(self.raw.as_ptr(), node.0, &mut ptr, &mut len) };
1949 Error::from_status(status)?;
1950 if len == 0 || ptr.is_null() {
1951 return Ok(Vec::new());
1952 }
1953 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1954 raw.iter().map(flat_node_from_ffi).collect()
1955 }
1956
1957 pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error> {
1962 let mut m = ffi::TwigQueryMatch {
1963 node_id: 0,
1964 span: ffi::TwigSpan { start: 0, end: 0 },
1965 content_span: ffi::TwigSpan { start: 0, end: 0 },
1966 has_content_span: 0,
1967 kind: std::ptr::null(),
1968 };
1969 let status = unsafe { ffi::twig_editor_node_at(self.raw.as_ptr(), offset, &mut m) };
1970 match status.0 {
1971 ffi::TwigStatus::OK => Ok(Some(query_match_from_ffi(&m)?)),
1972 ffi::TwigStatus::NOT_FOUND => Ok(None),
1973 _ => Err(Error::from_status(status).unwrap_err()),
1974 }
1975 }
1976
1977 pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error> {
1981 let mut ptr: *const ffi::TwigQueryMatch = std::ptr::null();
1982 let mut len = 0usize;
1983 let status =
1984 unsafe { ffi::twig_editor_nodes_at(self.raw.as_ptr(), offset, &mut ptr, &mut len) };
1985 match status.0 {
1986 ffi::TwigStatus::OK => {}
1987 ffi::TwigStatus::NOT_FOUND => return Ok(Vec::new()),
1988 _ => return Err(Error::from_status(status).unwrap_err()),
1989 }
1990 if len == 0 || ptr.is_null() {
1991 return Ok(Vec::new());
1992 }
1993 let raw = unsafe { std::slice::from_raw_parts(ptr, len) };
1994 raw.iter().map(query_match_from_ffi).collect()
1995 }
1996
1997 pub fn wrap_range(
2024 &mut self,
2025 start: usize,
2026 end: usize,
2027 kind: InlineKind,
2028 ) -> Result<Change, Error> {
2029 self.change_op(|ed, out| unsafe {
2030 ffi::twig_editor_wrap_range(ed, start, end, kind.to_c(), out)
2031 })
2032 }
2033
2034 pub fn toggle_inline(
2044 &mut self,
2045 start: usize,
2046 end: usize,
2047 kind: InlineKind,
2048 ) -> Result<Change, Error> {
2049 self.change_op(|ed, out| unsafe {
2050 ffi::twig_editor_toggle_inline(ed, start, end, kind.to_c(), out)
2051 })
2052 }
2053
2054 pub fn set_mark_color(
2092 &mut self,
2093 offset: usize,
2094 color: Option<MarkColor>,
2095 ) -> Result<Change, Error> {
2096 let name = color.map(MarkColor::as_str);
2097 let (ptr, len, has) = opt_str(name);
2098 self.change_op(|ed, out| unsafe {
2099 ffi::twig_editor_set_mark_color(ed, offset, ptr, len, has, out)
2100 })
2101 }
2102
2103 pub fn set_block(&mut self, offset: usize, kind: BlockKind) -> Result<Change, Error> {
2126 let (block_kind, level) = kind.to_c();
2127 self.change_op(|ed, out| unsafe {
2128 ffi::twig_editor_set_block(ed, offset, block_kind, level, out)
2129 })
2130 }
2131
2132 pub fn toggle_block_container(
2155 &mut self,
2156 start: usize,
2157 end: usize,
2158 kind: BlockContainerKind,
2159 ) -> Result<Change, Error> {
2160 self.change_op(|ed, out| unsafe {
2161 ffi::twig_editor_toggle_block_container(ed, start, end, kind.to_c(), out)
2162 })
2163 }
2164
2165 pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error> {
2184 self.change_op(|ed, out| unsafe {
2185 ffi::twig_editor_renumber_ordered_lists(ed, offset, out)
2186 })?;
2187 Ok(())
2188 }
2189
2190 pub fn table_insert_row(&mut self, offset: usize, below: bool) -> Result<(), Error> {
2199 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_ROW, below as c_int)
2200 }
2201
2202 pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error> {
2205 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_ROW, 0)
2206 }
2207
2208 pub fn table_insert_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
2210 self.table_edit(offset, ffi::TWIG_TABLE_INSERT_COLUMN, right as c_int)
2211 }
2212
2213 pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error> {
2215 self.table_edit(offset, ffi::TWIG_TABLE_DELETE_COLUMN, 0)
2216 }
2217
2218 pub fn table_set_alignment(
2220 &mut self,
2221 offset: usize,
2222 alignment: Alignment,
2223 ) -> Result<(), Error> {
2224 self.table_edit(offset, ffi::TWIG_TABLE_SET_ALIGNMENT, alignment.to_c())
2225 }
2226
2227 pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error> {
2229 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_ROW, down as c_int)
2230 }
2231
2232 pub fn table_move_column(&mut self, offset: usize, right: bool) -> Result<(), Error> {
2234 self.table_edit(offset, ffi::TWIG_TABLE_MOVE_COLUMN, right as c_int)
2235 }
2236
2237 fn table_edit(&mut self, offset: usize, op: c_int, arg: c_int) -> Result<(), Error> {
2238 self.change_op(|ed, out| unsafe { ffi::twig_editor_table_edit(ed, offset, op, arg, out) })?;
2239 Ok(())
2240 }
2241
2242 pub fn insert_table(
2261 &mut self,
2262 offset: usize,
2263 rows: usize,
2264 cols: usize,
2265 ) -> Result<Change, Error> {
2266 self.change_op(|ed, out| unsafe {
2267 ffi::twig_editor_insert_table(ed, offset, rows, cols, out)
2268 })
2269 }
2270
2271 pub fn insert_link(
2316 &mut self,
2317 start: usize,
2318 end: usize,
2319 destination: &str,
2320 ) -> Result<Change, Error> {
2321 self.change_op(|ed, out| unsafe {
2322 ffi::twig_editor_insert_link(
2323 ed,
2324 start,
2325 end,
2326 destination.as_ptr(),
2327 destination.len(),
2328 out,
2329 )
2330 })
2331 }
2332
2333 pub fn insert_image(
2354 &mut self,
2355 start: usize,
2356 end: usize,
2357 destination: &str,
2358 ) -> Result<Change, Error> {
2359 self.change_op(|ed, out| unsafe {
2360 ffi::twig_editor_insert_image(
2361 ed,
2362 start,
2363 end,
2364 destination.as_ptr(),
2365 destination.len(),
2366 out,
2367 )
2368 })
2369 }
2370
2371 pub fn insert_literal(&mut self, offset: usize, text: &str) -> Result<Change, Error> {
2396 self.change_op(|ed, out| unsafe {
2397 ffi::twig_editor_insert_literal(ed, offset, text.as_ptr(), text.len(), out)
2398 })
2399 }
2400
2401 pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error> {
2415 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_line_break(ed, offset, out) })
2416 }
2417
2418 pub fn insert_thematic_break(&mut self, offset: usize) -> Result<Change, Error> {
2437 self.change_op(|ed, out| unsafe { ffi::twig_editor_insert_thematic_break(ed, offset, out) })
2438 }
2439
2440 pub fn split_block(&mut self, offset: usize) -> Result<Change, Error> {
2488 self.change_op(|ed, out| unsafe { ffi::twig_editor_split_block(ed, offset, out) })
2489 }
2490
2491 pub fn toggle_code_block(
2523 &mut self,
2524 start: usize,
2525 end: usize,
2526 language: Option<&str>,
2527 ) -> Result<Change, Error> {
2528 let (ptr, len, has) = opt_str(language);
2529 self.change_op(|ed, out| unsafe {
2530 ffi::twig_editor_toggle_code_block(ed, start, end, ptr, len, has, out)
2531 })
2532 }
2533
2534 pub fn set_code_language(
2544 &mut self,
2545 offset: usize,
2546 language: Option<&str>,
2547 ) -> Result<Change, Error> {
2548 let (ptr, len, has) = opt_str(language);
2549 self.change_op(|ed, out| unsafe {
2550 ffi::twig_editor_set_code_language(ed, offset, ptr, len, has, out)
2551 })
2552 }
2553
2554 pub fn toggle_task_item(&mut self, offset: usize) -> Result<Change, Error> {
2565 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_item(ed, offset, out) })
2566 }
2567
2568 pub fn set_task_checked(&mut self, offset: usize, checked: bool) -> Result<(), Error> {
2582 self.change_op(|ed, out| unsafe {
2583 ffi::twig_editor_set_task_checked(ed, offset, checked as c_int, out)
2584 })?;
2585 Ok(())
2586 }
2587
2588 pub fn toggle_task_checked(&mut self, offset: usize) -> Result<Change, Error> {
2593 self.change_op(|ed, out| unsafe { ffi::twig_editor_toggle_task_checked(ed, offset, out) })
2594 }
2595
2596 pub fn insert_footnote(&mut self, offset: usize, label: &str) -> Result<Change, Error> {
2615 self.change_op(|ed, out| unsafe {
2616 ffi::twig_editor_insert_footnote(ed, offset, label.as_ptr(), label.len(), out)
2617 })
2618 }
2619
2620 fn change_op(
2623 &mut self,
2624 op: impl FnOnce(*mut ffi::TwigEditor, *mut ffi::TwigChange) -> ffi::TwigStatus,
2625 ) -> Result<Change, Error> {
2626 let mut change = ffi::TwigChange {
2627 old_span: ffi::TwigSpan { start: 0, end: 0 },
2628 new_span: ffi::TwigSpan { start: 0, end: 0 },
2629 };
2630 let status = op(self.raw.as_ptr(), &mut change);
2631 Error::from_status(status)?;
2632 Ok(Change::from_ffi(change))
2633 }
2634
2635 fn apply(
2637 &mut self,
2638 locator: &str,
2639 text: &str,
2640 op: impl FnOnce(*mut ffi::TwigEditor, *const u8, usize, *const u8, usize) -> ffi::TwigStatus,
2641 ) -> Result<(), Error> {
2642 let status = op(
2643 self.raw.as_ptr(),
2644 locator.as_ptr(),
2645 locator.len(),
2646 text.as_ptr(),
2647 text.len(),
2648 );
2649 Error::from_status(status)
2650 }
2651}
2652
2653impl Drop for Editor {
2654 fn drop(&mut self) {
2655 unsafe { ffi::twig_editor_destroy(self.raw.as_ptr()) }
2656 }
2657}
2658
2659fn collect_bytes(
2664 call: impl FnOnce(*mut *const u8, *mut usize) -> ffi::TwigStatus,
2665) -> Result<Vec<u8>, Error> {
2666 let mut ptr = std::ptr::null();
2667 let mut len = 0usize;
2668 let status = call(&mut ptr, &mut len);
2669 Error::from_status(status)?;
2670 if len == 0 {
2671 return Ok(Vec::new());
2672 }
2673 if ptr.is_null() {
2674 return Err(Error::Internal);
2675 }
2676 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2677 Ok(bytes.to_vec())
2678}
2679
2680fn collect_matches(
2683 call: impl FnOnce(*mut *const ffi::TwigQueryMatch, *mut usize) -> ffi::TwigStatus,
2684) -> Result<Vec<QueryMatch>, Error> {
2685 let mut ptr = std::ptr::null();
2686 let mut len = 0usize;
2687 let status = call(&mut ptr, &mut len);
2688 Error::from_status(status)?;
2689 if len == 0 {
2690 return Ok(Vec::new());
2691 }
2692 if ptr.is_null() {
2693 return Err(Error::Internal);
2694 }
2695 let matches = unsafe { std::slice::from_raw_parts(ptr, len) };
2696 matches.iter().map(query_match_from_ffi).collect()
2697}
2698
2699fn collect_flat_nodes(
2702 call: impl FnOnce(*mut *const ffi::TwigFlatNode, *mut usize) -> ffi::TwigStatus,
2703) -> Result<Vec<FlatNode>, Error> {
2704 let mut ptr = std::ptr::null();
2705 let mut len = 0usize;
2706 let status = call(&mut ptr, &mut len);
2707 Error::from_status(status)?;
2708 if len == 0 {
2709 return Ok(Vec::new());
2710 }
2711 if ptr.is_null() {
2712 return Err(Error::Internal);
2713 }
2714 let nodes = unsafe { std::slice::from_raw_parts(ptr, len) };
2715 nodes.iter().map(flat_node_from_ffi).collect()
2716}
2717
2718fn empty_ffi_match() -> ffi::TwigQueryMatch {
2720 ffi::TwigQueryMatch {
2721 node_id: 0,
2722 span: ffi::TwigSpan { start: 0, end: 0 },
2723 content_span: ffi::TwigSpan { start: 0, end: 0 },
2724 has_content_span: 0,
2725 kind: std::ptr::null(),
2726 }
2727}
2728
2729fn query_match_from_ffi(m: &ffi::TwigQueryMatch) -> Result<QueryMatch, Error> {
2732 Ok(QueryMatch {
2733 node_id: m.node_id,
2734 span: m.span.start..m.span.end,
2735 content_span: if m.has_content_span != 0 {
2736 Some(m.content_span.start..m.content_span.end)
2737 } else {
2738 None
2739 },
2740 kind: Kind::from(borrowed_cstr(m.kind)?.as_str()),
2741 })
2742}
2743
2744fn flat_node_from_ffi(n: &ffi::TwigFlatNode) -> Result<FlatNode, Error> {
2746 let node_id = |v: u32| {
2747 if v == ffi::TWIG_NO_NODE {
2748 None
2749 } else {
2750 Some(NodeId(v))
2751 }
2752 };
2753 Ok(FlatNode {
2754 id: NodeId(n.id),
2755 parent: node_id(n.parent),
2756 first_child: node_id(n.first_child),
2757 next_sibling: node_id(n.next_sibling),
2758 span: n.span.start..n.span.end,
2759 content_span: if n.has_content_span != 0 {
2760 Some(n.content_span.start..n.content_span.end)
2761 } else {
2762 None
2763 },
2764 level: if n.level != 0 { Some(n.level) } else { None },
2765 kind: Kind::from(borrowed_cstr(n.kind)?.as_str()),
2766 text: borrowed_bytes(n.text_ptr, n.text_len),
2767 destination: borrowed_bytes(n.destination_ptr, n.destination_len),
2768 head: match n.head {
2769 ffi::TWIG_HEAD_NONE => None,
2770 v => Some(v != 0),
2771 },
2772 alignment: Alignment::from_c(n.alignment),
2773 name: borrowed_bytes(n.name_ptr, n.name_len),
2774 directive_form: DirectiveForm::from_c(n.directive_form),
2775 origin: ContainerOrigin::from_c(n.container_origin),
2776 marker_span: if n.has_marker_span != 0 {
2777 Some(n.marker_span.start..n.marker_span.end)
2778 } else {
2779 None
2780 },
2781 checked: match n.checked {
2782 ffi::TWIG_TASK_CHECKED_NONE => None,
2783 v => Some(v != 0),
2784 },
2785 attrs: borrowed_attrs(n.attrs_ptr, n.attrs_len),
2786 })
2787}
2788
2789fn borrowed_attrs(ptr: *const ffi::TwigKeyVal, len: usize) -> Vec<(String, Option<String>)> {
2793 if ptr.is_null() || len == 0 {
2794 return Vec::new();
2795 }
2796 let kvs = unsafe { std::slice::from_raw_parts(ptr, len) };
2797 kvs.iter()
2798 .map(|kv| {
2799 let key = borrowed_bytes(kv.key, kv.key_len).unwrap_or_default();
2800 (key, borrowed_bytes(kv.value, kv.value_len))
2801 })
2802 .collect()
2803}
2804
2805fn borrowed_cstr(ptr: *const c_char) -> Result<String, Error> {
2807 if ptr.is_null() {
2808 return Err(Error::Internal);
2809 }
2810 Ok(unsafe { std::ffi::CStr::from_ptr(ptr) }
2811 .to_str()
2812 .map_err(|_| Error::Internal)?
2813 .to_owned())
2814}
2815
2816fn borrowed_bytes(ptr: *const u8, len: usize) -> Option<String> {
2820 if ptr.is_null() {
2821 return None;
2822 }
2823 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
2824 Some(String::from_utf8_lossy(bytes).into_owned())
2825}
2826
2827#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
2831pub struct NodeId(pub u32);
2832
2833#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2836pub enum VoidKind {
2837 Doc,
2838 Para,
2839 ThematicBreak,
2840 Section,
2841 Div,
2842 BlockQuote,
2843 DefinitionList,
2844 Table,
2845 ListItem,
2846 DefinitionListItem,
2847 Term,
2848 Definition,
2849 Caption,
2850 SoftBreak,
2851 HardBreak,
2852 NonBreakingSpace,
2853 Emph,
2854 Strong,
2855 Span,
2856 Mark,
2857 Superscript,
2858 Subscript,
2859 Insert,
2860 Delete,
2861 DoubleQuoted,
2862 SingleQuoted,
2863}
2864
2865impl VoidKind {
2866 fn to_c(self) -> c_int {
2867 match self {
2869 VoidKind::Doc => 0,
2870 VoidKind::Para => 1,
2871 VoidKind::ThematicBreak => 3,
2872 VoidKind::Section => 4,
2873 VoidKind::Div => 5,
2874 VoidKind::BlockQuote => 9,
2875 VoidKind::DefinitionList => 13,
2876 VoidKind::Table => 14,
2877 VoidKind::ListItem => 15,
2878 VoidKind::DefinitionListItem => 17,
2879 VoidKind::Term => 18,
2880 VoidKind::Definition => 19,
2881 VoidKind::Caption => 22,
2882 VoidKind::SoftBreak => 26,
2883 VoidKind::HardBreak => 27,
2884 VoidKind::NonBreakingSpace => 28,
2885 VoidKind::Emph => 38,
2886 VoidKind::Strong => 39,
2887 VoidKind::Span => 42,
2888 VoidKind::Mark => 43,
2889 VoidKind::Superscript => 44,
2890 VoidKind::Subscript => 45,
2891 VoidKind::Insert => 46,
2892 VoidKind::Delete => 47,
2893 VoidKind::DoubleQuoted => 48,
2894 VoidKind::SingleQuoted => 49,
2895 }
2896 }
2897}
2898
2899#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2901pub enum TextKind {
2902 Str,
2903 Symb,
2904 Verbatim,
2905 InlineMath,
2906 DisplayMath,
2907 Url,
2908 Email,
2909 FootnoteReference,
2910 CitationReference,
2913 SubstitutionReference,
2915 Comment,
2916 Doctype,
2917 Cdata,
2918}
2919
2920impl TextKind {
2921 fn to_c(self) -> c_int {
2922 match self {
2923 TextKind::Str => 25,
2924 TextKind::Symb => 29,
2925 TextKind::Verbatim => 30,
2926 TextKind::InlineMath => 32,
2927 TextKind::DisplayMath => 33,
2928 TextKind::Url => 34,
2929 TextKind::Email => 35,
2930 TextKind::FootnoteReference => 36,
2931 TextKind::CitationReference => 58,
2932 TextKind::SubstitutionReference => 59,
2933 TextKind::Comment => 52,
2934 TextKind::Doctype => 53,
2935 TextKind::Cdata => 55,
2936 }
2937 }
2938}
2939
2940#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2942pub enum BulletStyle {
2943 Dash,
2944 Plus,
2945 Star,
2946}
2947
2948impl BulletStyle {
2949 fn to_c(self) -> c_int {
2950 match self {
2951 BulletStyle::Dash => 0,
2952 BulletStyle::Plus => 1,
2953 BulletStyle::Star => 2,
2954 }
2955 }
2956}
2957
2958#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2960pub enum OrderedNumbering {
2961 Decimal,
2962 LowerAlpha,
2963 UpperAlpha,
2964 LowerRoman,
2965 UpperRoman,
2966}
2967
2968impl OrderedNumbering {
2969 fn to_c(self) -> c_int {
2970 match self {
2971 OrderedNumbering::Decimal => 0,
2972 OrderedNumbering::LowerAlpha => 1,
2973 OrderedNumbering::UpperAlpha => 2,
2974 OrderedNumbering::LowerRoman => 3,
2975 OrderedNumbering::UpperRoman => 4,
2976 }
2977 }
2978}
2979
2980#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2982pub enum OrderedDelim {
2983 Period,
2984 ParenAfter,
2985 ParenBoth,
2986}
2987
2988impl OrderedDelim {
2989 fn to_c(self) -> c_int {
2990 match self {
2991 OrderedDelim::Period => 0,
2992 OrderedDelim::ParenAfter => 1,
2993 OrderedDelim::ParenBoth => 2,
2994 }
2995 }
2996}
2997
2998#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3001pub enum Alignment {
3002 Default,
3003 Left,
3004 Right,
3005 Center,
3006}
3007
3008impl Alignment {
3009 fn to_c(self) -> c_int {
3010 match self {
3011 Alignment::Default => ffi::TWIG_ALIGN_DEFAULT,
3012 Alignment::Left => ffi::TWIG_ALIGN_LEFT,
3013 Alignment::Right => ffi::TWIG_ALIGN_RIGHT,
3014 Alignment::Center => ffi::TWIG_ALIGN_CENTER,
3015 }
3016 }
3017
3018 fn from_c(v: c_int) -> Option<Self> {
3021 match v {
3022 ffi::TWIG_ALIGN_DEFAULT => Some(Alignment::Default),
3023 ffi::TWIG_ALIGN_LEFT => Some(Alignment::Left),
3024 ffi::TWIG_ALIGN_RIGHT => Some(Alignment::Right),
3025 ffi::TWIG_ALIGN_CENTER => Some(Alignment::Center),
3026 _ => None,
3027 }
3028 }
3029}
3030
3031#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3033pub enum SmartPunctuation {
3034 LeftSingleQuote,
3035 RightSingleQuote,
3036 LeftDoubleQuote,
3037 RightDoubleQuote,
3038 Ellipses,
3039 EmDash,
3040 EnDash,
3041}
3042
3043impl SmartPunctuation {
3044 fn to_c(self) -> c_int {
3045 match self {
3046 SmartPunctuation::LeftSingleQuote => 0,
3047 SmartPunctuation::RightSingleQuote => 1,
3048 SmartPunctuation::LeftDoubleQuote => 2,
3049 SmartPunctuation::RightDoubleQuote => 3,
3050 SmartPunctuation::Ellipses => 4,
3051 SmartPunctuation::EmDash => 5,
3052 SmartPunctuation::EnDash => 6,
3053 }
3054 }
3055}
3056
3057#[derive(Clone, Debug, Eq, PartialEq)]
3072pub struct Warning {
3073 pub fidelity: Fidelity,
3074 pub path: String,
3081 pub kind: Kind,
3084}
3085
3086#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3088#[non_exhaustive]
3089pub enum Fidelity {
3090 Degraded,
3093 Dropped,
3095}
3096
3097impl Fidelity {
3098 fn from_c(v: c_int) -> Self {
3102 match v {
3103 ffi::TWIG_FIDELITY_DROPPED => Fidelity::Dropped,
3104 _ => Fidelity::Degraded,
3105 }
3106 }
3107}
3108
3109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3110#[non_exhaustive]
3111pub enum ContainerOrigin {
3112 Element,
3114 Directive,
3118}
3119
3120impl ContainerOrigin {
3121 fn from_c(v: c_int) -> Option<Self> {
3124 match v {
3125 ffi::TWIG_CONTAINER_ORIGIN_ELEMENT => Some(ContainerOrigin::Element),
3126 ffi::TWIG_CONTAINER_ORIGIN_DIRECTIVE => Some(ContainerOrigin::Directive),
3127 _ => None,
3128 }
3129 }
3130}
3131
3132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3134pub enum DirectiveForm {
3135 Text,
3136 Leaf,
3137 Container,
3138}
3139
3140impl DirectiveForm {
3141 fn to_c(self) -> c_int {
3142 match self {
3143 DirectiveForm::Text => ffi::TWIG_DIRECTIVE_TEXT,
3144 DirectiveForm::Leaf => ffi::TWIG_DIRECTIVE_LEAF,
3145 DirectiveForm::Container => ffi::TWIG_DIRECTIVE_CONTAINER,
3146 }
3147 }
3148
3149 fn from_c(v: c_int) -> Option<Self> {
3153 match v {
3154 ffi::TWIG_DIRECTIVE_TEXT => Some(DirectiveForm::Text),
3155 ffi::TWIG_DIRECTIVE_LEAF => Some(DirectiveForm::Leaf),
3156 ffi::TWIG_DIRECTIVE_CONTAINER => Some(DirectiveForm::Container),
3157 _ => None,
3158 }
3159 }
3160}
3161
3162fn opt_str(s: Option<&str>) -> (*const u8, usize, c_int) {
3166 match s {
3167 Some(x) => (x.as_ptr(), x.len(), 1),
3168 None => (std::ptr::null(), 0, 0),
3169 }
3170}
3171
3172#[derive(Debug)]
3179pub struct Builder {
3180 raw: NonNull<ffi::TwigBuilder>,
3181}
3182
3183impl Builder {
3184 pub fn new() -> Result<Self, Error> {
3186 let mut raw = std::ptr::null_mut();
3187 let status = unsafe { ffi::twig_builder_create(&mut raw) };
3188 Error::from_status(status)?;
3189 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
3190 Ok(Self { raw })
3191 }
3192
3193 pub fn add(&mut self, kind: VoidKind) -> Result<NodeId, Error> {
3196 self.emit(|b, out| unsafe { ffi::twig_builder_add(b, kind.to_c(), out) })
3197 }
3198
3199 pub fn add_text(&mut self, kind: TextKind, text: &str) -> Result<NodeId, Error> {
3201 self.emit(|b, out| unsafe {
3202 ffi::twig_builder_add_text(b, kind.to_c(), text.as_ptr(), text.len(), out)
3203 })
3204 }
3205
3206 pub fn add_heading(&mut self, level: u32) -> Result<NodeId, Error> {
3208 self.emit(|b, out| unsafe { ffi::twig_builder_add_heading(b, level, out) })
3209 }
3210
3211 pub fn add_code_block(&mut self, lang: Option<&str>, text: &str) -> Result<NodeId, Error> {
3213 let (lp, ll, has) = opt_str(lang);
3214 self.emit(|b, out| unsafe {
3215 ffi::twig_builder_add_code_block(b, lp, ll, has, text.as_ptr(), text.len(), out)
3216 })
3217 }
3218
3219 pub fn add_raw_block(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
3221 self.emit(|b, out| unsafe {
3222 ffi::twig_builder_add_raw_block(
3223 b,
3224 format.as_ptr(),
3225 format.len(),
3226 text.as_ptr(),
3227 text.len(),
3228 out,
3229 )
3230 })
3231 }
3232
3233 pub fn add_metadata(&mut self, lang: &str, text: &str) -> Result<NodeId, Error> {
3235 self.emit(|b, out| unsafe {
3236 ffi::twig_builder_add_metadata(
3237 b,
3238 lang.as_ptr(),
3239 lang.len(),
3240 text.as_ptr(),
3241 text.len(),
3242 out,
3243 )
3244 })
3245 }
3246
3247 pub fn add_raw_inline(&mut self, format: &str, text: &str) -> Result<NodeId, Error> {
3249 self.emit(|b, out| unsafe {
3250 ffi::twig_builder_add_raw_inline(
3251 b,
3252 format.as_ptr(),
3253 format.len(),
3254 text.as_ptr(),
3255 text.len(),
3256 out,
3257 )
3258 })
3259 }
3260
3261 pub fn add_smart_punctuation(
3266 &mut self,
3267 kind: SmartPunctuation,
3268 text: &str,
3269 ) -> Result<NodeId, Error> {
3270 self.emit(|b, out| unsafe {
3271 ffi::twig_builder_add_smart_punctuation(b, kind.to_c(), text.as_ptr(), text.len(), out)
3272 })
3273 }
3274
3275 pub fn add_link(
3278 &mut self,
3279 destination: Option<&str>,
3280 reference: Option<&str>,
3281 ) -> Result<NodeId, Error> {
3282 let (dp, dl, hd) = opt_str(destination);
3283 let (rp, rl, hr) = opt_str(reference);
3284 self.emit(|b, out| unsafe { ffi::twig_builder_add_link(b, dp, dl, hd, rp, rl, hr, out) })
3285 }
3286
3287 pub fn add_image(
3289 &mut self,
3290 destination: Option<&str>,
3291 reference: Option<&str>,
3292 ) -> Result<NodeId, Error> {
3293 let (dp, dl, hd) = opt_str(destination);
3294 let (rp, rl, hr) = opt_str(reference);
3295 self.emit(|b, out| unsafe { ffi::twig_builder_add_image(b, dp, dl, hd, rp, rl, hr, out) })
3296 }
3297
3298 pub fn add_directive(&mut self, form: DirectiveForm, name: &str) -> Result<NodeId, Error> {
3300 self.emit(|b, out| unsafe {
3301 ffi::twig_builder_add_directive(b, form.to_c(), name.as_ptr(), name.len(), out)
3302 })
3303 }
3304
3305 pub fn add_element(&mut self, name: &str) -> Result<NodeId, Error> {
3307 self.emit(|b, out| unsafe {
3308 ffi::twig_builder_add_element(b, name.as_ptr(), name.len(), out)
3309 })
3310 }
3311
3312 pub fn add_processing_instruction(
3314 &mut self,
3315 target: &str,
3316 data: &str,
3317 ) -> Result<NodeId, Error> {
3318 self.emit(|b, out| unsafe {
3319 ffi::twig_builder_add_processing_instruction(
3320 b,
3321 target.as_ptr(),
3322 target.len(),
3323 data.as_ptr(),
3324 data.len(),
3325 out,
3326 )
3327 })
3328 }
3329
3330 pub fn add_footnote(&mut self, label: &str) -> Result<NodeId, Error> {
3332 self.emit(|b, out| unsafe {
3333 ffi::twig_builder_add_footnote(b, label.as_ptr(), label.len(), out)
3334 })
3335 }
3336
3337 pub fn add_citation(&mut self, label: &str) -> Result<NodeId, Error> {
3342 self.emit(|b, out| unsafe {
3343 ffi::twig_builder_add_citation(b, label.as_ptr(), label.len(), out)
3344 })
3345 }
3346
3347 pub fn add_substitution(&mut self, label: &str) -> Result<NodeId, Error> {
3351 self.emit(|b, out| unsafe {
3352 ffi::twig_builder_add_substitution(b, label.as_ptr(), label.len(), out)
3353 })
3354 }
3355
3356 pub fn add_reference(&mut self, label: &str, destination: &str) -> Result<NodeId, Error> {
3358 self.emit(|b, out| unsafe {
3359 ffi::twig_builder_add_reference(
3360 b,
3361 label.as_ptr(),
3362 label.len(),
3363 destination.as_ptr(),
3364 destination.len(),
3365 out,
3366 )
3367 })
3368 }
3369
3370 pub fn add_bullet_list(&mut self, style: BulletStyle, tight: bool) -> Result<NodeId, Error> {
3372 self.emit(|b, out| unsafe {
3373 ffi::twig_builder_add_bullet_list(b, style.to_c(), tight as c_int, out)
3374 })
3375 }
3376
3377 pub fn add_ordered_list(
3379 &mut self,
3380 numbering: OrderedNumbering,
3381 delim: OrderedDelim,
3382 tight: bool,
3383 start: Option<u32>,
3384 ) -> Result<NodeId, Error> {
3385 let (start_val, has_start) = match start {
3386 Some(s) => (s, 1),
3387 None => (0, 0),
3388 };
3389 self.emit(|b, out| unsafe {
3390 ffi::twig_builder_add_ordered_list(
3391 b,
3392 numbering.to_c(),
3393 delim.to_c(),
3394 tight as c_int,
3395 start_val,
3396 has_start,
3397 out,
3398 )
3399 })
3400 }
3401
3402 pub fn add_task_list(&mut self, tight: bool) -> Result<NodeId, Error> {
3404 self.emit(|b, out| unsafe { ffi::twig_builder_add_task_list(b, tight as c_int, out) })
3405 }
3406
3407 pub fn add_task_list_item(&mut self, checked: bool) -> Result<NodeId, Error> {
3409 self.emit(|b, out| unsafe {
3410 ffi::twig_builder_add_task_list_item(b, checked as c_int, out)
3411 })
3412 }
3413
3414 pub fn add_row(&mut self, head: bool) -> Result<NodeId, Error> {
3416 self.emit(|b, out| unsafe { ffi::twig_builder_add_row(b, head as c_int, out) })
3417 }
3418
3419 pub fn add_cell(&mut self, head: bool, alignment: Alignment) -> Result<NodeId, Error> {
3421 self.emit(|b, out| unsafe {
3422 ffi::twig_builder_add_cell(b, head as c_int, alignment.to_c(), out)
3423 })
3424 }
3425
3426 pub fn add_cell_spanning(
3431 &mut self,
3432 head: bool,
3433 alignment: Alignment,
3434 colspan: u32,
3435 rowspan: u32,
3436 ) -> Result<NodeId, Error> {
3437 self.emit(|b, out| unsafe {
3438 ffi::twig_builder_add_cell_spanning(
3439 b,
3440 head as c_int,
3441 alignment.to_c(),
3442 colspan,
3443 rowspan,
3444 out,
3445 )
3446 })
3447 }
3448
3449 pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), Error> {
3452 let ids: Vec<u32> = children.iter().map(|n| n.0).collect();
3453 let status = unsafe {
3454 ffi::twig_builder_set_children(self.raw.as_ptr(), parent.0, ids.as_ptr(), ids.len())
3455 };
3456 Error::from_status(status)
3457 }
3458
3459 pub fn set_attrs(&mut self, id: NodeId, attrs: &[(&str, Option<&str>)]) -> Result<(), Error> {
3463 let kvs: Vec<ffi::TwigKeyVal> = attrs
3464 .iter()
3465 .map(|(k, v)| ffi::TwigKeyVal {
3466 key: k.as_ptr(),
3467 key_len: k.len(),
3468 value: v.map_or(std::ptr::null(), |s| s.as_ptr()),
3469 value_len: v.map_or(0, |s| s.len()),
3470 })
3471 .collect();
3472 let status = unsafe {
3473 ffi::twig_builder_set_attrs(self.raw.as_ptr(), id.0, kvs.as_ptr(), kvs.len())
3474 };
3475 Error::from_status(status)
3476 }
3477
3478 pub fn render_html(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3481 let raw = self.raw.as_ptr();
3482 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_render_html(raw, root.0, ptr, len) })
3483 }
3484
3485 pub fn serialize_to(&mut self, root: NodeId, target: Target) -> Result<Vec<u8>, Error> {
3492 let raw = self.raw.as_ptr();
3493 let ffi_target: ffi::TwigFormat = target.into();
3494 collect_bytes(|ptr, len| unsafe {
3495 ffi::twig_builder_serialize(raw, root.0, ffi_target as i32, ptr, len)
3496 })
3497 }
3498
3499 pub fn serialize(&mut self, root: NodeId, format: Format) -> Result<Vec<u8>, Error> {
3504 self.serialize_to(root, format.into())
3505 }
3506
3507 pub fn ast_json(&mut self, root: NodeId) -> Result<Vec<u8>, Error> {
3509 let raw = self.raw.as_ptr();
3510 collect_bytes(|ptr, len| unsafe { ffi::twig_builder_ast_json(raw, root.0, ptr, len) })
3511 }
3512
3513 pub fn query(&mut self, root: NodeId, selector: &str) -> Result<Vec<QueryMatch>, Error> {
3516 let raw = self.raw.as_ptr();
3517 collect_matches(|ptr, len| unsafe {
3518 ffi::twig_builder_query(raw, root.0, selector.as_ptr(), selector.len(), ptr, len)
3519 })
3520 }
3521
3522 fn emit(
3525 &mut self,
3526 call: impl FnOnce(*mut ffi::TwigBuilder, *mut u32) -> ffi::TwigStatus,
3527 ) -> Result<NodeId, Error> {
3528 let mut id: u32 = 0;
3529 let status = call(self.raw.as_ptr(), &mut id);
3530 Error::from_status(status)?;
3531 Ok(NodeId(id))
3532 }
3533}
3534
3535impl Drop for Builder {
3536 fn drop(&mut self) {
3537 unsafe { ffi::twig_builder_destroy(self.raw.as_ptr()) }
3538 }
3539}
3540
3541#[cfg(test)]
3542mod tests {
3543 use super::*;
3544
3545 #[test]
3546 fn abi_version_matches() {
3547 assert_eq!(abi_version(), ffi::TWIG_ABI_VERSION);
3551 }
3552
3553 #[test]
3554 fn parses_and_renders_markdown_html() {
3555 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3556 let html = doc.render_html().expect("render html");
3557 assert_eq!(String::from_utf8_lossy(&html), "<h1>hi</h1>\n");
3558 }
3559
3560 #[test]
3561 fn parses_html_input() {
3562 let mut doc = Document::parse_str("<p>hi</p>", Format::Html).expect("parse html");
3563 let html = doc.render_html().expect("render html");
3564 assert!(String::from_utf8_lossy(&html).contains("hi"));
3565 }
3566
3567 #[test]
3568 fn parses_renders_and_writes_asciidoc() {
3569 let mut doc = Document::parse_str("= Title\n\nsome *bold* text\n", Format::Asciidoc)
3570 .expect("parse asciidoc");
3571 let html = String::from_utf8_lossy(&doc.render_html().expect("render html")).into_owned();
3572 assert!(html.contains("<h1>Title</h1>"), "got {html:?}");
3573 assert!(html.contains("<strong>bold</strong>"), "got {html:?}");
3574
3575 let back = doc.serialize_to(Target::Asciidoc).expect("serialize asciidoc");
3577 assert_eq!(String::from_utf8_lossy(&back), "= Title\n\nsome *bold* text\n");
3578 let mut md = Document::parse_str("# Title\n\nsome **bold** text\n", Format::Markdown)
3579 .expect("parse markdown");
3580 let converted = md.serialize_to(Target::Asciidoc).expect("convert to asciidoc");
3581 assert_eq!(String::from_utf8_lossy(&converted), "= Title\n\nsome *bold* text\n");
3582 assert_eq!(Target::from(Format::Asciidoc), Target::Asciidoc);
3583 assert_eq!(Target::Asciidoc.as_format(), Some(Format::Asciidoc));
3584 }
3585
3586 #[test]
3587 fn markdown_dialects_are_formats_over_one_parser() {
3588 let src = "a ~~b~~ c\n\n| x |\n| - |\n| $m$ |\n";
3592 let count = |doc: &mut Document, sel: &str| doc.query(sel).expect("query").len();
3593 for (format, ext, delete, table, math) in [
3594 (Format::Commonmark, MarkdownExtensions::default(), 0, 0, 0),
3595 (Format::Markdown, MarkdownExtensions::default(), 1, 1, 0),
3596 (Format::Gfm, MarkdownExtensions::default(), 1, 1, 0),
3597 (Format::Gfm, MarkdownExtensions { math: true, ..Default::default() }, 1, 1, 1),
3598 ] {
3599 let mut doc = Document::parse_str_with(src, format, ext).expect("parse");
3600 assert_eq!(count(&mut doc, "delete"), delete, "{format:?} {ext:?}");
3601 assert_eq!(count(&mut doc, "table"), table, "{format:?} {ext:?}");
3602 assert_eq!(count(&mut doc, "inline_math"), math, "{format:?} {ext:?}");
3603 }
3604
3605 assert_eq!(Format::Gfm.dialect_of(), Some(Format::Markdown));
3608 assert_eq!(Format::Commonmark.dialect_of(), Some(Format::Markdown));
3609 assert_eq!(Format::Markdown.dialect_of(), None);
3610 assert_eq!(Target::from(Format::Gfm), Target::Markdown);
3611 let mut gfm = Document::parse_str("* a ~~b~~\n", Format::Gfm).expect("parse gfm");
3612 let back = gfm.serialize(Format::Gfm).expect("serialize");
3613 assert_eq!(String::from_utf8_lossy(&back), "* a ~~b~~\n");
3614
3615 let mut table = Document::parse_str("| a |\n| :-: |\n| 1 |\n", Format::Gfm).expect("parse");
3617 let html = String::from_utf8_lossy(&table.render_html().expect("render")).into_owned();
3618 assert!(html.contains("align=\"center\""), "got {html:?}");
3619
3620 assert!(!Format::Commonmark.supports(Gesture::ToggleInline(InlineKind::Delete)));
3622 assert!(Format::Gfm.supports(Gesture::ToggleInline(InlineKind::Delete)));
3623 }
3624
3625 #[test]
3626 fn serialize_round_trips_and_cross_converts() {
3627 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3628
3629 let canonical = doc.serialize(Format::Markdown).expect("serialize markdown");
3630 assert!(String::from_utf8_lossy(&canonical).contains("# hi"));
3631
3632 assert_eq!(doc.serialize(Format::Xml), Err(Error::UnsupportedFormat));
3634 }
3635
3636 #[test]
3637 fn serialize_markdown_to_djot() {
3638 let mut doc =
3639 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3640 let djot = doc.serialize(Format::Djot).expect("serialize djot");
3641 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3642 }
3643
3644 #[test]
3645 fn serialize_to_takes_the_output_axis() {
3646 let mut doc =
3647 Document::parse_str("This is *markdown*.\n", Format::Markdown).expect("parse markdown");
3648
3649 let djot = doc.serialize_to(Target::Djot).expect("serialize djot");
3650 assert!(String::from_utf8_lossy(&djot).contains("_markdown_"));
3651
3652 assert_eq!(doc.serialize_to(Target::Xml), Err(Error::UnsupportedFormat));
3655 }
3656
3657 #[test]
3658 fn serialize_and_serialize_to_agree() {
3659 let mut a = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3662 let mut b = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
3663 for format in [Format::Markdown, Format::Djot, Format::Html] {
3664 assert_eq!(a.serialize(format), b.serialize_to(Target::from(format)));
3665 }
3666 }
3667
3668 #[test]
3669 fn every_format_is_a_target_that_names_it_back() {
3670 for format in [Format::Djot, Format::Markdown, Format::Xml, Format::Html] {
3673 assert_eq!(Target::from(format).as_format(), Some(format));
3674 }
3675 }
3676
3677 #[test]
3678 fn ast_json_dumps_the_tree() {
3679 let mut doc = Document::parse_str("hello\n", Format::Djot).expect("parse djot");
3680 let json = doc.ast_json().expect("ast json");
3681 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
3682 }
3683
3684 #[test]
3685 fn query_finds_nodes_by_selector() {
3686 let source = "# One\n\n## Two\n";
3687 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3688 let matches = doc.query("heading").expect("query");
3689
3690 assert_eq!(matches.len(), 2);
3691 for m in &matches {
3692 assert_eq!(m.kind, Kind::Heading);
3693 assert!(m.span.start < m.span.end);
3694 }
3695 }
3696
3697 #[test]
3698 fn query_recovers_code_spans() {
3699 let source = "prose `code` more prose\n";
3700 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3701 let matches = doc.query("verbatim").expect("query");
3702
3703 assert_eq!(matches.len(), 1);
3704 assert_eq!(&source[matches[0].span.clone()], "`code`");
3705 }
3706
3707 #[test]
3708 fn document_span_accessors_read_by_node_id() {
3709 let source = "# hi\n\ntext\n";
3710 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3711 let heading = doc.query("heading").expect("query").pop().expect("heading");
3712
3713 assert_eq!(
3714 doc.span(NodeId(heading.node_id)).expect("span"),
3715 heading.span
3716 );
3717 assert_eq!(
3718 doc.content_span(NodeId(heading.node_id))
3719 .expect("content span"),
3720 heading.content_span
3721 );
3722 assert_eq!(doc.span(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3723 }
3724
3725 #[test]
3726 fn document_walks_its_tree_without_an_editor() {
3727 let source = "# hi\n\ntext\n";
3728 let mut doc = Document::parse_str(source, Format::Markdown).expect("parse markdown");
3729
3730 let nodes = doc.nodes().expect("nodes");
3731 assert!(nodes.len() >= 3);
3732 for (i, n) in nodes.iter().enumerate() {
3733 assert_eq!(n.id, NodeId(i as u32));
3734 }
3735
3736 let kids = doc.children(None).expect("children");
3737 assert_eq!(kids.len(), 2);
3738 assert_eq!(kids[0].kind, Kind::Heading);
3739
3740 let sub = doc.subtree(NodeId(kids[0].node_id)).expect("subtree");
3741 assert_eq!(sub[0].id, NodeId(0));
3742 assert_eq!(sub[0].parent, None);
3743 assert_eq!(sub[0].span, kids[0].span);
3744
3745 let hit = doc.node_at(2).expect("node_at").expect("a node at 2");
3746 let chain = doc.ancestors_at(2).expect("ancestors");
3747 assert_eq!(chain.last().expect("deepest").node_id, hit.node_id);
3748 assert_eq!(chain[0].kind, Kind::Doc);
3749
3750 assert_eq!(doc.subtree(NodeId(u32::MAX)), Err(Error::InvalidArgument));
3751 }
3752
3753 #[test]
3754 fn editor_document_view_reads_the_live_tree() {
3755 let mut ed = Editor::new_str("# one\n\ntwo\n", Format::Markdown).expect("editor");
3756
3757 {
3758 let mut view = ed.document().expect("view");
3759 let kids = view.children(None).expect("children");
3760 assert_eq!(kids.len(), 2);
3761 assert_eq!(kids[0].kind, Kind::Heading);
3762 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..5);
3763 assert_eq!(view.render_html(), Err(Error::UnsupportedFormat));
3765 assert_eq!(
3766 view.serialize(Format::Markdown),
3767 Err(Error::UnsupportedFormat)
3768 );
3769 }
3770
3771 ed.replace("0", "# one and a half").expect("replace");
3772 let mut view = ed.document().expect("view");
3773 let kids = view.children(None).expect("children");
3774 assert_eq!(view.span(NodeId(kids[0].node_id)).expect("span"), 0..16);
3775 }
3776
3777 #[test]
3778 fn query_rejects_a_malformed_selector() {
3779 let mut doc = Document::parse_str("hi\n", Format::Markdown).expect("parse markdown");
3780 assert_eq!(doc.query("list >"), Err(Error::InvalidArgument));
3781 }
3782
3783 #[test]
3784 fn editor_edits_by_index_path() {
3785 let mut ed = Editor::new_str("<a><b>hi</b></a>", Format::Xml).expect("editor");
3786 ed.replace_content("0.0", "bye").expect("replace_content");
3787 assert_eq!(ed.source_str().expect("source"), "<a><b>bye</b></a>");
3788 }
3789
3790 #[test]
3791 fn flat_nodes_expose_element_name_and_attrs() {
3792 let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"x\"></picture>\n";
3796 let mut ed = Editor::new_ext(
3797 src.as_bytes(),
3798 Format::Markdown,
3799 MarkdownExtensions {
3800 html_elements: true,
3801 ..Default::default()
3802 },
3803 )
3804 .expect("editor");
3805 let nodes = ed.nodes().expect("nodes");
3806
3807 let source = nodes
3808 .iter()
3809 .find(|n| n.name.as_deref() == Some("source"))
3810 .expect("a <source> element node");
3811 assert_eq!(
3812 source.attrs,
3813 vec![
3814 (
3815 "media".to_string(),
3816 Some("(prefers-color-scheme: dark)".to_string())
3817 ),
3818 ("srcset".to_string(), Some("d.svg".to_string())),
3819 ]
3820 );
3821
3822 let img = nodes
3825 .iter()
3826 .find(|n| n.kind == Kind::Image)
3827 .expect("an image node");
3828 assert!(img.name.is_none());
3829 assert_eq!(img.destination.as_deref(), Some("l.svg"));
3830
3831 let picture_kids_str = nodes.iter().find(|n| n.kind == Kind::Str);
3833 if let Some(s) = picture_kids_str {
3834 assert!(s.name.is_none() && s.attrs.is_empty());
3835 }
3836 }
3837
3838 #[test]
3839 fn definitions_finds_what_a_walk_from_the_root_cannot() {
3840 let mut doc = Document::parse_str(
3844 "text[^1] [x][a]\n\n[^1]: note\n\n[a]: /u\n",
3845 Format::Markdown,
3846 )
3847 .expect("parse markdown");
3848
3849 let defs = doc.definitions().expect("definitions");
3850 let mut kinds: Vec<Kind> = defs.iter().map(|m| m.kind.clone()).collect();
3851 kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3852 assert_eq!(kinds, vec![Kind::Footnote, Kind::Reference]);
3853
3854 for d in &defs {
3858 let want = match d.kind {
3859 Kind::Footnote => 17..27,
3860 Kind::Reference => 29..36,
3861 _ => unreachable!(),
3862 };
3863 assert_eq!(d.span, want, "{} stands on its own bytes", d.kind);
3864 }
3865
3866 let all = doc.nodes().expect("nodes");
3869 let root = all
3870 .iter()
3871 .find(|n| n.kind == Kind::Doc)
3872 .expect("a doc root");
3873 let mut reachable = vec![root.id];
3874 let mut i = 0;
3875 while i < reachable.len() {
3876 let n = &all[reachable[i].0 as usize];
3877 let mut c = n.first_child;
3878 while let Some(cid) = c {
3879 reachable.push(cid);
3880 c = all[cid.0 as usize].next_sibling;
3881 }
3882 i += 1;
3883 }
3884 for d in &defs {
3885 assert!(
3886 !reachable.contains(&NodeId(d.node_id)),
3887 "{} should be unreachable from the root",
3888 d.kind
3889 );
3890 }
3891
3892 let mut plain = Document::parse_str("just text\n", Format::Markdown).expect("parse");
3894 assert_eq!(plain.definitions().expect("definitions"), Vec::new());
3895 }
3896
3897 #[test]
3898 fn kind_round_trips_through_its_published_name() {
3899 for k in [
3903 Kind::Doc,
3904 Kind::Para,
3905 Kind::Heading,
3906 Kind::Container,
3907 Kind::TaskListItem,
3908 Kind::Superscript,
3909 Kind::FootnoteReference,
3910 Kind::ProcessingInstruction,
3911 Kind::Cdata,
3912 ] {
3913 assert_eq!(Kind::from(k.as_str()), k, "{k} did not round-trip");
3914 assert!(!k.is_unknown());
3915 }
3916 }
3917
3918 #[test]
3919 fn an_unknown_kind_name_is_carried_rather_than_lost() {
3920 let k = Kind::from("some_future_kind");
3923 assert!(k.is_unknown());
3924 assert_eq!(k.as_str(), "some_future_kind");
3925 assert_eq!(k, Kind::Other("some_future_kind".to_string()));
3926 }
3927
3928 #[test]
3929 fn every_kind_the_library_publishes_has_a_variant() {
3930 let cases: &[(&str, Format, MarkdownExtensions)] = &[
3935 (
3936 "# 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",
3937 Format::Markdown,
3938 MarkdownExtensions {
3939 directives: false,
3940 math: false,
3941 html_elements: false,
3942 highlight: false,
3943 highlight_colors: false,
3944 },
3945 ),
3946 (
3947 "| 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",
3948 Format::Markdown,
3949 MarkdownExtensions::default(),
3950 ),
3951 (
3952 ":::note\nbody\n:::\n\n:role[x]\n\n$a+b$ ==h== ==🔴 r==\n",
3953 Format::Markdown,
3954 MarkdownExtensions {
3955 directives: true,
3956 math: true,
3957 html_elements: false,
3958 highlight: true,
3959 highlight_colors: true,
3960 },
3961 ),
3962 (
3963 "a^b^ c~d~ {=e=} {+f+} {-g-} 'q' \"dq\"\n\n\n\n<https://e.com>\n",
3964 Format::Djot,
3965 MarkdownExtensions::default(),
3966 ),
3967 (
3968 "<!-- c --><!DOCTYPE html><video controls><p>x</p></video>",
3969 Format::Html,
3970 MarkdownExtensions::default(),
3971 ),
3972 ];
3973
3974 let mut unknown: Vec<String> = Vec::new();
3975 let mut seen: Vec<String> = Vec::new();
3976 for (src, format, ext) in cases {
3977 let mut ed = Editor::new_ext(src.as_bytes(), *format, *ext).expect("editor");
3978 for n in ed.nodes().expect("nodes") {
3979 if n.kind.is_unknown() {
3980 unknown.push(n.kind.as_str().to_string());
3981 }
3982 seen.push(n.kind.as_str().to_string());
3983 }
3984 }
3985 unknown.sort();
3986 unknown.dedup();
3987 assert!(unknown.is_empty(), "kinds with no variant: {unknown:?}");
3988
3989 seen.sort();
3992 seen.dedup();
3993 assert!(
3994 seen.len() >= 30,
3995 "only {} distinct kinds reached: {seen:?}",
3996 seen.len()
3997 );
3998 }
3999
4000 #[test]
4001 fn diagnostics_report_what_a_conversion_would_lose() {
4002 let mut doc = Document::parse_str("a^b^ c\n", Format::Djot).expect("parse djot");
4006
4007 let to_md = doc
4008 .diagnostics(Target::Markdown)
4009 .expect("markdown diagnostics");
4010 assert_eq!(
4011 to_md,
4012 vec![Warning {
4013 fidelity: Fidelity::Degraded,
4014 path: "0/1".to_string(),
4015 kind: Kind::Superscript,
4016 }]
4017 );
4018
4019 assert_eq!(
4021 doc.diagnostics(Target::Djot).expect("djot diagnostics"),
4022 Vec::new()
4023 );
4024 }
4025
4026 #[test]
4027 fn diagnostics_separate_a_droppable_node_from_a_degradable_one() {
4028 let mut doc =
4032 Document::parse_str("<p>hi</p><!-- secret -->", Format::Html).expect("parse html");
4033 let warnings = doc.diagnostics(Target::Djot).expect("djot diagnostics");
4034 let comment = warnings
4035 .iter()
4036 .find(|w| w.kind == Kind::Comment)
4037 .expect("a warning about the comment");
4038 assert_eq!(comment.fidelity, Fidelity::Dropped);
4039 }
4040
4041 #[test]
4042 fn diagnostics_refuse_a_target_with_no_serializer() {
4043 let mut doc = Document::parse_str("# hi\n", Format::Markdown).expect("parse markdown");
4046 assert_eq!(doc.diagnostics(Target::Xml), Err(Error::UnsupportedFormat));
4047 assert!(doc.diagnostics(Target::Asciidoc).is_ok());
4049 }
4050
4051 #[test]
4052 fn diagnostics_flag_a_header_less_table_and_leave_a_headed_one_alone() {
4053 let mut headed = Document::parse_str(
4058 "<table><tr><th>H</th></tr><tr><td>a</td></tr></table>",
4059 Format::Html,
4060 )
4061 .expect("parse headed table");
4062 assert!(
4063 headed
4064 .diagnostics(Target::Markdown)
4065 .expect("diagnostics")
4066 .iter()
4067 .all(|w| w.kind != Kind::Table)
4068 );
4069
4070 let mut headless = Document::parse_str("<table><tr><td>a</td></tr></table>", Format::Html)
4071 .expect("parse header-less table");
4072 let table_warning = headless
4073 .diagnostics(Target::Markdown)
4074 .expect("diagnostics")
4075 .into_iter()
4076 .find(|w| w.kind == Kind::Table)
4077 .expect("a warning about the table");
4078 assert_eq!(table_warning.fidelity, Fidelity::Degraded);
4079 }
4080
4081 #[test]
4082 fn container_origin_separates_a_div_from_a_div() {
4083 let mut html =
4088 Editor::new("<div>hi</div>\n".as_bytes(), Format::Html).expect("html editor");
4089 let mut md = Editor::new_ext(
4090 ":::div\nhi\n:::\n".as_bytes(),
4091 Format::Markdown,
4092 MarkdownExtensions {
4093 directives: true,
4094 ..Default::default()
4095 },
4096 )
4097 .expect("markdown editor");
4098
4099 let html_nodes = html.nodes().expect("html nodes");
4100 let md_nodes = md.nodes().expect("markdown nodes");
4101 let tag = html_nodes
4102 .iter()
4103 .find(|n| n.name.as_deref() == Some("div"))
4104 .expect("a <div> container");
4105 let directive = md_nodes
4106 .iter()
4107 .find(|n| n.name.as_deref() == Some("div"))
4108 .expect("a :::div container");
4109
4110 assert_eq!(tag.kind, directive.kind);
4112 assert_eq!(tag.name, directive.name);
4113 assert_eq!(tag.directive_form, directive.directive_form);
4114 assert_eq!(tag.directive_form, Some(DirectiveForm::Container));
4115
4116 assert_eq!(tag.origin, Some(ContainerOrigin::Element));
4118 assert_eq!(directive.origin, Some(ContainerOrigin::Directive));
4119 }
4120
4121 #[test]
4122 fn a_container_whose_body_is_text_says_so_in_text() {
4123 let mut html = Editor::new(
4129 "<script>a < b</script><title>a & b</title><span>a & b</span>\n".as_bytes(),
4130 Format::Html,
4131 )
4132 .expect("html editor");
4133 let nodes = html.nodes().expect("html nodes");
4134 let by_name = |name: &str| {
4135 nodes
4136 .iter()
4137 .find(|n| n.name.as_deref() == Some(name))
4138 .unwrap_or_else(|| panic!("a <{name}> container"))
4139 };
4140 let script = by_name("script");
4141 assert_eq!(script.kind, Kind::Container);
4142 assert_eq!(script.text.as_deref(), Some("a < b"));
4143 assert_eq!(script.first_child, None);
4144 assert_eq!(by_name("title").text.as_deref(), Some("a & b"));
4146 let span = by_name("span");
4148 assert_eq!(span.text, None);
4149 assert!(span.first_child.is_some());
4150 }
4151
4152 fn for_both_formats(src: &str, check: impl Fn(&mut Document, Format)) {
4156 for format in [Format::Markdown, Format::Djot] {
4157 let mut doc = Document::parse(src.as_bytes(), format).expect("parse");
4158 check(&mut doc, format);
4159 }
4160 }
4161
4162 #[test]
4163 fn marker_span_is_what_a_rich_view_hides() {
4164 for_both_formats("> - [x] done\n", |doc, format| {
4165 let nodes = doc.nodes().expect("nodes");
4166 let quote = nodes
4167 .iter()
4168 .find(|n| n.kind == Kind::BlockQuote)
4169 .expect("a block quote");
4170 let item = nodes
4171 .iter()
4172 .find(|n| n.kind == Kind::TaskListItem)
4173 .expect("a task item");
4174
4175 assert_eq!(quote.marker_span, Some(0..2), "{format:?}");
4179 assert_eq!(item.marker_span, Some(2..8), "{format:?}");
4180
4181 assert_eq!(quote.content_span, Some(quote.span.clone()), "{format:?}");
4185
4186 let para = nodes
4188 .iter()
4189 .find(|n| n.kind == Kind::Para)
4190 .expect("a paragraph");
4191 assert_eq!(para.marker_span, None, "{format:?}");
4192 });
4193 }
4194
4195 #[test]
4196 fn attrs_span_locates_the_attribute_block_a_heuristic_had_to_guess_at() {
4197 let src = "{.vis .family}\nheld back\n\nplain\n";
4203 let mut doc = Document::parse(src.as_bytes(), Format::Djot).expect("parse");
4204 let nodes = doc.nodes().expect("nodes");
4205 let paras: Vec<&FlatNode> = nodes.iter().filter(|n| n.kind == Kind::Para).collect();
4206 assert_eq!(paras.len(), 2);
4207
4208 let span = doc
4209 .attrs_span(paras[0].id)
4210 .expect("attrs span")
4211 .expect("the attributed paragraph has one");
4212 assert_eq!(&src[span.clone()], "{.vis .family}");
4213 assert!(span.end <= paras[0].span.start);
4216
4217 assert_eq!(doc.attrs_span(paras[1].id).expect("attrs span"), None);
4220 }
4221
4222 #[test]
4223 fn line_prefix_assembles_every_marker_on_the_line() {
4224 for_both_formats("> - [x] done\n", |doc, format| {
4225 assert_eq!(doc.line_prefix(9).expect("prefix"), Some(0..8), "{format:?}");
4228 });
4229 }
4230
4231 #[test]
4232 fn line_prefix_is_none_on_a_continuation_line() {
4233 for_both_formats("> c\n> d\n", |doc, format| {
4239 assert_eq!(doc.line_prefix(2).expect("prefix"), Some(0..2), "{format:?}");
4240 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
4241 });
4242 }
4243
4244 #[test]
4245 fn a_caret_at_a_blocks_end_is_in_that_block_in_both_formats() {
4246 for_both_formats("a\n\nb\n", |doc, format| {
4252 for offset in [0usize, 1, 3, 4] {
4253 let hit = doc
4254 .node_at_caret(offset)
4255 .expect("caret hit")
4256 .expect("some node");
4257 assert_eq!(hit.kind, Kind::Str, "{format:?} at {offset}");
4258 }
4259 for offset in [2usize, 5] {
4262 let hit = doc
4263 .node_at_caret(offset)
4264 .expect("caret hit")
4265 .expect("some node");
4266 assert_eq!(hit.kind, Kind::Doc, "{format:?} at {offset}");
4267 }
4268 });
4269 }
4270
4271 #[test]
4272 fn the_caret_chain_ends_at_the_node_the_scalar_call_returns() {
4273 for_both_formats("- a\n", |doc, format| {
4274 let hit = doc.node_at_caret(3).expect("hit").expect("some node");
4275 let chain = doc.ancestors_at_caret(3).expect("chain");
4276 assert_eq!(chain.last().map(|m| m.node_id), Some(hit.node_id), "{format:?}");
4277 assert!(
4280 chain.iter().any(|m| m.kind == Kind::ListItem),
4281 "{format:?}: chain should reach the list item"
4282 );
4283 });
4284 }
4285
4286 #[test]
4287 fn continuation_prefix_repeats_a_quote_and_indents_past_an_item() {
4288 for_both_formats("> - a\n", |doc, format| {
4289 assert_eq!(doc.line_prefix(4).expect("prefix"), Some(0..4), "{format:?}");
4294 let cont = doc.continuation_prefix(4).expect("continuation");
4295 assert_eq!(cont.text, "> ", "{format:?}");
4296 assert_eq!(cont.columns, 4, "{format:?}");
4297 });
4298 }
4299
4300 #[test]
4301 fn continuation_prefix_answers_on_a_line_that_opens_nothing() {
4302 for_both_formats("> c\n> d\n", |doc, format| {
4305 assert_eq!(doc.line_prefix(6).expect("prefix"), None, "{format:?}");
4306 assert_eq!(
4307 doc.continuation_prefix(6).expect("continuation").text,
4308 "> ",
4309 "{format:?}"
4310 );
4311 });
4312 }
4313
4314 #[test]
4315 fn continuation_prefix_takes_an_ordered_markers_own_width() {
4316 for_both_formats("10. x\n", |doc, format| {
4319 assert_eq!(
4320 doc.continuation_prefix(4).expect("continuation").columns,
4321 4,
4322 "{format:?}"
4323 );
4324 });
4325 for_both_formats("1. x\n", |doc, format| {
4326 assert_eq!(
4327 doc.continuation_prefix(3).expect("continuation").columns,
4328 3,
4329 "{format:?}"
4330 );
4331 });
4332 }
4333
4334 #[test]
4335 fn a_blank_line_keeps_a_quote_alive_and_drops_an_items_indent() {
4336 for_both_formats("> - a\n", |doc, format| {
4337 let blank = doc.blank_line_prefix(4).expect("blank");
4338 assert_eq!(blank.text, ">", "{format:?}");
4341 assert_eq!(blank.columns, 1, "{format:?}");
4342 });
4343 for_both_formats("- a\n", |doc, format| {
4346 assert_eq!(doc.blank_line_prefix(3).expect("blank").text, "", "{format:?}");
4347 });
4348 }
4349
4350 #[test]
4351 fn a_prefix_column_count_is_not_its_byte_length() {
4352 let mut doc = Document::parse("- x
4355".as_bytes(), Format::Markdown).expect("parse");
4356 let cont = doc.continuation_prefix(2).expect("continuation");
4357 assert_eq!(cont.columns, 4);
4358 }
4359
4360 #[test]
4361 fn set_block_opens_a_heading_on_a_blank_line() {
4362 for format in [Format::Markdown, Format::Djot] {
4363 let mut ed = Editor::new("a\n\n".as_bytes(), format).expect("editor");
4364 ed.set_block(3, BlockKind::Heading(2)).expect("set_block");
4365 assert_eq!(ed.source().expect("source"), b"a\n\n## ", "{format:?}");
4366 let nodes = ed.nodes().expect("nodes");
4370 assert!(
4371 nodes.iter().any(|n| n.kind == Kind::Heading),
4372 "{format:?}: should have parsed a heading"
4373 );
4374 }
4375 }
4376
4377 #[test]
4378 fn set_block_refuses_a_blank_line_inside_a_code_block() {
4379 for format in [Format::Markdown, Format::Djot] {
4383 let src = "```\nx\n\ny\n```\n";
4384 let mut ed = Editor::new(src.as_bytes(), format).expect("editor");
4385 let blank = src.find("\n\n").expect("a blank line") + 1;
4386 assert!(
4387 matches!(
4388 ed.set_block(blank, BlockKind::Heading(1)),
4389 Err(Error::NotEditable)
4390 ),
4391 "{format:?}"
4392 );
4393 assert_eq!(ed.source().expect("source"), src.as_bytes(), "{format:?}");
4394 }
4395 }
4396
4397 #[test]
4398 fn task_items_report_their_checkbox_state() {
4399 for_both_formats("- [ ] a\n- [x] b\n- [X] c\n- d\n", |doc, format| {
4403 let nodes = doc.nodes().expect("nodes");
4404 let states: Vec<Option<bool>> = nodes
4405 .iter()
4406 .filter(|n| matches!(n.kind, Kind::TaskListItem | Kind::ListItem))
4407 .map(|n| n.checked)
4408 .collect();
4409 assert_eq!(
4410 states,
4411 vec![Some(false), Some(true), Some(true), None],
4412 "{format:?}"
4413 );
4414
4415 for n in nodes.iter().filter(|n| n.kind == Kind::Para) {
4418 assert_eq!(n.checked, None, "{format:?}");
4419 }
4420 });
4421 }
4422
4423 #[test]
4424 fn an_editor_reaches_the_caret_reads_through_its_document_view() {
4425 let mut ed = Editor::new("- a\n".as_bytes(), Format::Markdown).expect("editor");
4430 let mut view = ed.document().expect("document view");
4431
4432 assert_eq!(view.line_prefix(3).expect("prefix"), Some(0..2));
4433 let hit = view.node_at_caret(3).expect("hit").expect("some node");
4434 assert_eq!(hit.kind, Kind::Str);
4435 }
4436
4437 #[test]
4438 fn container_origin_is_none_for_non_containers() {
4439 let mut ed = Editor::new("# hi\n\npara\n".as_bytes(), Format::Markdown).expect("editor");
4442 for n in ed.nodes().expect("nodes") {
4443 assert_eq!(n.origin, None, "{} should carry no origin", n.kind);
4444 }
4445 }
4446
4447 #[test]
4448 fn flat_nodes_expose_directive_name_and_form() {
4449 let src = ":::note{.warning}\nBody\n:::\n\n::embed{src=\"demo.html\"}\n\nSee :abbr[HTML]{title=\"HyperText\"} inline.\n";
4455 let mut ed = Editor::new_ext(
4456 src.as_bytes(),
4457 Format::Markdown,
4458 MarkdownExtensions {
4459 directives: true,
4460 ..Default::default()
4461 },
4462 )
4463 .expect("editor");
4464 let nodes = ed.nodes().expect("nodes");
4465
4466 let forms: Vec<(Option<&str>, Option<DirectiveForm>)> = nodes
4467 .iter()
4468 .filter(|n| n.kind == Kind::Container)
4469 .map(|n| (n.name.as_deref(), n.directive_form))
4470 .collect();
4471 assert_eq!(
4472 forms,
4473 vec![
4474 (Some("note"), Some(DirectiveForm::Container)),
4475 (Some("embed"), Some(DirectiveForm::Leaf)),
4476 (Some("abbr"), Some(DirectiveForm::Text)),
4477 ]
4478 );
4479
4480 let embed = nodes
4483 .iter()
4484 .find(|n| n.name.as_deref() == Some("embed"))
4485 .expect("embed");
4486 assert_eq!(
4487 embed.attrs,
4488 vec![("src".to_string(), Some("demo.html".to_string()))]
4489 );
4490 let para = nodes.iter().find(|n| n.kind == Kind::Para).expect("a para");
4491 assert!(para.directive_form.is_none() && para.name.is_none());
4492 }
4493
4494 #[test]
4495 fn editor_insert_child_and_delete() {
4496 let mut ed = Editor::new_str("<r><a/><c/></r>", Format::Xml).expect("editor");
4497 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4498 assert_eq!(ed.source_str().expect("source"), "<r><a/><b/><c/></r>");
4499 ed.delete("0.1").expect("delete");
4500 assert_eq!(ed.source_str().expect("source"), "<r><a/><c/></r>");
4501 }
4502
4503 #[test]
4504 fn editor_edits_by_selector() {
4505 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4506 ed.replace("heading(\"Two\")", "## Renamed")
4507 .expect("replace");
4508 assert_eq!(ed.source_str().expect("source"), "# One\n\n## Renamed\n");
4509 }
4510
4511 #[test]
4512 fn editor_locator_errors_are_distinct() {
4513 let mut ed = Editor::new_str("<r><a/><a/></r>", Format::Xml).expect("editor");
4514 assert_eq!(ed.replace("0.9", "x"), Err(Error::NotFound));
4515 assert_eq!(ed.replace("element", "x"), Err(Error::Ambiguous));
4516 assert_eq!(ed.replace("element(", "x"), Err(Error::InvalidArgument));
4517 assert_eq!(ed.source_str().expect("source"), "<r><a/><a/></r>");
4519 }
4520
4521 #[test]
4522 fn editor_reparse_break_rolls_back() {
4523 let mut ed = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
4524 assert_eq!(ed.replace_content("0", "<b>"), Err(Error::EditConflict));
4525 assert_eq!(ed.source_str().expect("source"), "<a>ok</a>");
4526 }
4527
4528 #[test]
4529 fn editor_leaf_content_is_not_editable() {
4530 let mut ed = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
4531 assert_eq!(ed.replace_content("0.0", "x"), Err(Error::NotEditable));
4532 }
4533
4534 #[test]
4535 fn editor_query_reflects_current_tree() {
4536 let mut ed = Editor::new_str("<r><a/></r>", Format::Xml).expect("editor");
4537 ed.insert_child("0", 1, "<b/>").expect("insert_child");
4538 assert_eq!(ed.query("element").expect("query").len(), 3);
4540 let json = ed.ast_json().expect("ast_json");
4541 assert!(String::from_utf8_lossy(&json).contains("\"kind\": \"doc\""));
4542 }
4543
4544 #[test]
4547 fn editor_edit_range_types_backspaces_and_reports_change() {
4548 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
4549
4550 let c = ed.edit_range(1, 1, "X").expect("edit_range insert");
4552 assert_eq!(ed.source_str().unwrap(), "aXb\n");
4553 assert_eq!(c.old, 1..1);
4554 assert_eq!(c.new, 1..2);
4555 assert_eq!(c.delta(), 1);
4556
4557 let c2 = ed.edit_range(1, 2, "").expect("edit_range delete");
4559 assert_eq!(ed.source_str().unwrap(), "ab\n");
4560 assert_eq!(c2.old, 1..2);
4561 assert_eq!(c2.new, 1..1);
4562 assert_eq!(c2.delta(), -1);
4563 }
4564
4565 #[test]
4566 fn editor_edit_range_rejects_bad_ranges() {
4567 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
4568 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"); }
4572
4573 #[test]
4574 fn editor_last_change_reports_locator_ops_too() {
4575 let mut ed = Editor::new_str("# One\n\n## Two\n", Format::Markdown).expect("editor");
4576 assert_eq!(ed.last_change(), None); ed.replace("heading(\"Two\")", "## Renamed")
4579 .expect("replace");
4580 assert_eq!(ed.source_str().unwrap(), "# One\n\n## Renamed\n");
4581 let c = ed.last_change().expect("a change was recorded");
4582 assert_eq!(c.old, 7..13);
4584 assert_eq!(c.new, 7..17);
4585 }
4586
4587 #[test]
4588 fn editor_nodes_is_a_walkable_flat_tree() {
4589 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4590 let nodes = ed.nodes().expect("nodes");
4591 assert!(!nodes.is_empty());
4592
4593 for (i, n) in nodes.iter().enumerate() {
4595 assert_eq!(n.id, NodeId(i as u32));
4596 }
4597 let roots: Vec<_> = nodes.iter().filter(|n| n.parent.is_none()).collect();
4599 assert_eq!(roots.len(), 1);
4600 assert_eq!(roots[0].kind, Kind::Doc);
4601
4602 let heading = nodes
4604 .iter()
4605 .find(|n| n.kind == Kind::Heading)
4606 .expect("a heading");
4607 assert_eq!(heading.level, Some(1));
4608 assert!(nodes.iter().any(|n| n.text.as_deref() == Some("Hi")));
4609
4610 assert_eq!(heading.head, None);
4612 assert_eq!(heading.alignment, None);
4613
4614 for n in nodes.iter().filter(|n| n.parent.is_some()) {
4617 let p = &nodes[n.parent.unwrap().0 as usize];
4618 let mut kid = p.first_child;
4619 let mut seen = false;
4620 while let Some(NodeId(k)) = kid {
4621 if k == n.id.0 {
4622 seen = true;
4623 break;
4624 }
4625 kid = nodes[k as usize].next_sibling;
4626 }
4627 assert!(
4628 seen,
4629 "node {:?} not found among its parent's children",
4630 n.id
4631 );
4632 }
4633 }
4634
4635 #[test]
4636 fn editor_child_spans_and_subtree_agree_with_nodes() {
4637 let src = "# Title\n\nHello **world** and more.\n\n- one\n- two\n";
4638 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4639 let all = ed.nodes().expect("nodes");
4640 let doc = all.iter().find(|n| n.kind == Kind::Doc).expect("doc");
4641
4642 let top = ed.child_spans(None).expect("child_spans");
4645 let mut want = Vec::new();
4646 let mut c = doc.first_child;
4647 while let Some(id) = c {
4648 want.push(id);
4649 c = all[id.0 as usize].next_sibling;
4650 }
4651 assert_eq!(top.len(), want.len(), "top-level count");
4652 for (m, id) in top.iter().zip(&want) {
4653 assert_eq!(m.node_id, id.0, "child id");
4654 assert_eq!(m.kind, all[id.0 as usize].kind, "child kind");
4655 assert_eq!(m.span, all[id.0 as usize].span, "child span");
4656 }
4657 assert!(
4659 src[top[0].span.clone()].starts_with('#'),
4660 "first block is the heading"
4661 );
4662
4663 let list = top
4665 .iter()
4666 .find(|m| {
4667 matches!(
4668 m.kind,
4669 Kind::BulletList | Kind::OrderedList | Kind::TaskList
4670 )
4671 })
4672 .expect("a list");
4673 let items = ed.child_spans(Some(NodeId(list.node_id))).expect("items");
4674 assert_eq!(items.len(), 2);
4675 assert!(
4676 items.iter().all(|m| m.kind == Kind::ListItem),
4677 "items: {items:?}"
4678 );
4679
4680 let para = top
4682 .iter()
4683 .find(|m| m.kind == Kind::Para)
4684 .expect("a para")
4685 .node_id;
4686 let sub = ed.subtree(NodeId(para)).expect("subtree");
4687 assert_eq!(sub[0].id, NodeId(0), "root is local id 0");
4688 assert_eq!(sub[0].parent, None, "root has no parent inside the subtree");
4689 assert_eq!(sub[0].next_sibling, None, "root's sibling is severed");
4690 assert_eq!(sub[0].kind, Kind::Para);
4691 for (i, n) in sub.iter().enumerate() {
4692 assert_eq!(n.id, NodeId(i as u32), "dense local ids");
4693 for link in [n.parent, n.first_child, n.next_sibling]
4694 .into_iter()
4695 .flatten()
4696 {
4697 assert!(
4698 (link.0 as usize) < sub.len(),
4699 "link {link:?} escapes the subtree"
4700 );
4701 }
4702 }
4703 assert!(
4704 src[sub[0].span.clone()].starts_with("Hello"),
4705 "absolute span: {:?}",
4706 &src[sub[0].span.clone()]
4707 );
4708
4709 fn arena_kinds(all: &[FlatNode], root: NodeId) -> Vec<Kind> {
4711 let mut out = Vec::new();
4712 let mut stack = vec![root];
4713 while let Some(id) = stack.pop() {
4714 let n = &all[id.0 as usize];
4715 out.push(n.kind.clone());
4716 let mut c = n.first_child;
4717 while let Some(cid) = c {
4718 stack.push(cid);
4719 c = all[cid.0 as usize].next_sibling;
4720 }
4721 }
4722 out
4723 }
4724 let mut want_kinds = arena_kinds(&all, NodeId(para));
4725 let mut got_kinds: Vec<Kind> = sub.iter().map(|n| n.kind.clone()).collect();
4726 want_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4730 got_kinds.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4731 assert_eq!(got_kinds, want_kinds, "subtree kinds match the arena");
4732
4733 assert!(matches!(
4735 ed.subtree(NodeId(9999)),
4736 Err(Error::InvalidArgument)
4737 ));
4738 }
4739
4740 #[test]
4741 fn flat_nodes_carry_table_head_and_alignment() {
4742 let src = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n";
4746 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
4747 let nodes = ed.nodes().expect("nodes");
4748
4749 let rows: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Row).collect();
4750 assert_eq!(rows.len(), 2, "a header row and one body row");
4751 assert_eq!(rows[0].head, Some(true), "first row is the header");
4752 assert_eq!(rows[1].head, Some(false), "second row is a body row");
4753
4754 let cells: Vec<_> = nodes.iter().filter(|n| n.kind == Kind::Cell).collect();
4755 assert_eq!(cells.len(), 4);
4756 assert_eq!(cells[0].alignment, Some(Alignment::Left));
4758 assert_eq!(cells[1].alignment, Some(Alignment::Right));
4759 assert_eq!(cells[2].alignment, Some(Alignment::Left));
4760 assert_eq!(cells[3].alignment, Some(Alignment::Right));
4761 assert_eq!(cells[0].head, Some(true));
4763 assert_eq!(cells[2].head, Some(false));
4764
4765 let mut plain =
4768 Editor::new_str("| A |\n| --- |\n| b |\n", Format::Markdown).expect("editor");
4769 let pnodes = plain.nodes().expect("nodes");
4770 let pcell = pnodes
4771 .iter()
4772 .find(|n| n.kind == Kind::Cell)
4773 .expect("a cell");
4774 assert_eq!(pcell.alignment, Some(Alignment::Default));
4775 }
4776
4777 #[test]
4778 fn cell_extent_reports_merged_cells_and_nothing_else() {
4779 let src = "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td><td>b</td></tr></table>";
4780 let mut doc = Document::parse_str(src, Format::Html).expect("parse");
4781 let cells: Vec<NodeId> = doc
4782 .nodes()
4783 .expect("nodes")
4784 .iter()
4785 .filter(|n| n.kind == Kind::Cell)
4786 .map(|n| n.id)
4787 .collect();
4788 assert_eq!(cells.len(), 2);
4789 assert_eq!(doc.cell_extent(cells[0]).expect("extent"), Some((2, 3)));
4790 assert_eq!(doc.cell_extent(cells[1]).expect("extent"), Some((1, 1)));
4792
4793 let mut pipe =
4795 Document::parse_str("| a |\n| --- |\n| b |\n", Format::Markdown).expect("parse");
4796 let pipe_cell = pipe
4797 .nodes()
4798 .expect("nodes")
4799 .iter()
4800 .find(|n| n.kind == Kind::Cell)
4801 .expect("a cell")
4802 .id;
4803 assert_eq!(pipe.cell_extent(pipe_cell).expect("extent"), Some((1, 1)));
4804
4805 let root = NodeId(0);
4807 assert_eq!(pipe.cell_extent(root).expect("extent"), None);
4808 }
4809
4810 #[test]
4811 fn builder_add_cell_spanning_renders_colspan_and_rowspan() {
4812 let mut b = Builder::new().expect("builder");
4813 let wide_text = b.add_text(TextKind::Str, "wide").expect("str");
4814 let wide = b
4815 .add_cell_spanning(false, Alignment::Default, 2, 3)
4816 .expect("cell");
4817 b.set_children(wide, &[wide_text]).expect("children");
4818 let plain_text = b.add_text(TextKind::Str, "one").expect("str");
4819 let plain = b.add_cell(false, Alignment::Default).expect("cell");
4820 b.set_children(plain, &[plain_text]).expect("children");
4821 let row = b.add_row(false).expect("row");
4822 b.set_children(row, &[wide, plain]).expect("children");
4823 let table = b.add(VoidKind::Table).expect("table");
4824 b.set_children(table, &[row]).expect("children");
4825
4826 let html = String::from_utf8(b.render_html(table).expect("html")).expect("utf-8");
4827 assert!(
4828 html.contains("<td colspan=\"2\" rowspan=\"3\">wide</td>"),
4829 "{html}"
4830 );
4831 assert!(html.contains("<td>one</td>"), "{html}");
4833
4834 assert!(matches!(
4836 b.add_cell_spanning(false, Alignment::Default, 0, 1),
4837 Err(Error::InvalidArgument)
4838 ));
4839 }
4840
4841 #[test]
4842 fn editor_node_at_and_ancestors_hit_test_offsets() {
4843 let mut ed = Editor::new_str("# Hi\n\ntext\n", Format::Markdown).expect("editor");
4844
4845 let m = ed
4847 .node_at(2)
4848 .expect("node_at")
4849 .expect("a node covers offset 2");
4850 assert!(m.span.contains(&2));
4851
4852 let chain = ed.ancestors_at(2).expect("ancestors_at");
4854 assert!(!chain.is_empty());
4855 assert_eq!(chain[0].kind, Kind::Doc);
4856 assert_eq!(chain.last().unwrap().node_id, m.node_id);
4857
4858 assert_eq!(ed.node_at(999), Err(Error::InvalidArgument));
4860 }
4861
4862 #[test]
4865 fn editor_wrap_and_toggle_inline_round_trip() {
4866 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4867
4868 let c = ed.wrap_range(2, 6, InlineKind::Strong).expect("wrap");
4870 assert_eq!(ed.source_str().unwrap(), "a **word** b\n");
4871 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**word**");
4872
4873 ed.toggle_inline(4, 8, InlineKind::Strong)
4875 .expect("toggle off");
4876 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4877
4878 ed.toggle_inline(2, 6, InlineKind::Emph).expect("toggle on");
4880 assert_eq!(ed.source_str().unwrap(), "a *word* b\n");
4881 }
4882
4883 #[test]
4884 fn editor_inline_marks_cut_at_block_boundaries() {
4885 let mut ed = Editor::new_str("one two\n\nthree four\n", Format::Markdown)
4888 .expect("editor");
4889 let c = ed.toggle_inline(0, 19, InlineKind::Strong).expect("toggle on");
4890 assert_eq!(
4891 ed.source_str().unwrap(),
4892 "**one two**\n\n**three four**\n"
4893 );
4894
4895 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "**one two**\n\n**three four**");
4898 ed.undo().expect("undo");
4899 assert_eq!(ed.source_str().unwrap(), "one two\n\nthree four\n");
4900
4901 ed.toggle_inline(0, 19, InlineKind::Strong).expect("toggle on");
4904 ed.toggle_inline(0, 27, InlineKind::Strong).expect("toggle off");
4905 assert_eq!(ed.source_str().unwrap(), "one two\n\nthree four\n");
4906
4907 let mut fenced = Editor::new_str("```\nx y\n```\n", Format::Markdown).expect("editor");
4909 assert_eq!(
4910 fenced.toggle_inline(4, 7, InlineKind::Strong),
4911 Err(Error::NotEditable)
4912 );
4913 }
4914
4915 #[test]
4916 fn editor_inline_kind_support_is_format_specific() {
4917 let mut md = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4919 assert_eq!(
4920 md.wrap_range(2, 6, InlineKind::Mark),
4921 Err(Error::UnsupportedFormat)
4922 );
4923
4924 let mut dj = Editor::new_str("a word b\n", Format::Djot).expect("editor");
4926 dj.wrap_range(2, 6, InlineKind::Mark).expect("djot mark");
4927 assert_eq!(dj.source_str().unwrap(), "a {=word=} b\n");
4928 }
4929
4930 #[test]
4931 fn editor_authors_gfm_strikethrough_out_of_the_box() {
4932 assert!(Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Delete)));
4936 let mut ed = Editor::new_str("a word b\n", Format::Markdown).expect("editor");
4937 ed.toggle_inline(2, 6, InlineKind::Delete).expect("strike");
4938 assert_eq!(ed.source_str().unwrap(), "a ~~word~~ b\n");
4939 ed.toggle_inline(4, 8, InlineKind::Delete).expect("unstrike");
4940 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4941 }
4942
4943 #[test]
4944 fn editor_highlight_is_authorable_with_the_extension_on() {
4945 let exts = MarkdownExtensions {
4946 highlight: true,
4947 ..Default::default()
4948 };
4949 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
4953 assert!(Format::Markdown.supports_with(exts, Gesture::ToggleInline(InlineKind::Mark)));
4954
4955 let mut ed =
4956 Editor::new_ext(b"a word b\n", Format::Markdown, exts).expect("editor");
4957 ed.toggle_inline(2, 6, InlineKind::Mark).expect("highlight");
4958 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4959 ed.toggle_inline(4, 8, InlineKind::Mark).expect("unhighlight");
4960 assert_eq!(ed.source_str().unwrap(), "a word b\n");
4961 }
4962
4963 #[test]
4964 fn editor_set_mark_color_writes_reads_and_clears_the_colour() {
4965 let exts = MarkdownExtensions {
4966 highlight: true,
4967 highlight_colors: true,
4968 ..Default::default()
4969 };
4970 assert!(Format::Markdown.supports_with(exts, Gesture::SetMarkColor));
4971 let hi_only = MarkdownExtensions {
4973 highlight: true,
4974 ..Default::default()
4975 };
4976 assert!(!Format::Markdown.supports_with(hi_only, Gesture::SetMarkColor));
4977 assert!(!Format::Markdown.supports(Gesture::SetMarkColor));
4978 assert!(!Format::Djot.supports_with(exts, Gesture::SetMarkColor));
4979
4980 let mut ed =
4981 Editor::new_ext("a ==word== b\n".as_bytes(), Format::Markdown, exts).expect("editor");
4982 ed.set_mark_color(6, Some(MarkColor::Red)).expect("colour");
4983 assert_eq!(ed.source_str().unwrap(), "a ==\u{1F534} word== b\n");
4984
4985 let mut doc =
4987 Document::parse_with(ed.source_str().unwrap().as_bytes(), Format::Markdown, exts)
4988 .expect("parse");
4989 assert_eq!(doc.query("mark[data-color=red]").expect("query").len(), 1);
4990
4991 ed.set_mark_color(9, Some(MarkColor::Blue)).expect("recolour");
4992 assert_eq!(ed.source_str().unwrap(), "a ==\u{1F535} word== b\n");
4993 ed.set_mark_color(9, None).expect("clear");
4994 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
4995
4996 assert_eq!(
4998 ed.set_mark_color(0, Some(MarkColor::Red)),
4999 Err(Error::NotEditable)
5000 );
5001 assert_eq!(ed.source_str().unwrap(), "a ==word== b\n");
5002
5003 for c in [
5005 MarkColor::Red,
5006 MarkColor::Orange,
5007 MarkColor::Yellow,
5008 MarkColor::Green,
5009 MarkColor::Blue,
5010 MarkColor::Purple,
5011 MarkColor::Brown,
5012 ] {
5013 assert_eq!(MarkColor::from_str(c.as_str()), Some(c));
5014 }
5015 assert_eq!(MarkColor::from_str("pink"), None);
5016 }
5017
5018 #[test]
5019 fn editor_toggle_strips_verbatim_via_content_span() {
5020 let mut ed = Editor::new_str("a `code` b\n", Format::Markdown).expect("editor");
5021 ed.toggle_inline(2, 8, InlineKind::Verbatim)
5023 .expect("toggle code off");
5024 assert_eq!(ed.source_str().unwrap(), "a code b\n");
5025
5026 let mut ed2 = Editor::new_str("a ``x`` b\n", Format::Markdown).expect("editor");
5029 ed2.toggle_inline(2, 7, InlineKind::Verbatim)
5030 .expect("toggle multi off");
5031 assert_eq!(ed2.source_str().unwrap(), "a x b\n");
5032 }
5033
5034 #[test]
5035 fn editor_set_block_switches_para_and_heading_levels() {
5036 let mut ed = Editor::new_str("Title\n\nbody text\n", Format::Markdown).expect("editor");
5037
5038 ed.set_block(0, BlockKind::Heading(2)).expect("to h2");
5040 assert_eq!(ed.source_str().unwrap(), "## Title\n\nbody text\n");
5041
5042 ed.set_block(3, BlockKind::Heading(1)).expect("to h1");
5044 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody text\n");
5045
5046 ed.set_block(2, BlockKind::Paragraph).expect("to para");
5048 assert_eq!(ed.source_str().unwrap(), "Title\n\nbody text\n");
5049 }
5050
5051 #[test]
5052 fn editor_set_block_rejects_bad_level_and_format() {
5053 let mut md = Editor::new_str("hi\n", Format::Markdown).expect("editor");
5054 assert_eq!(
5055 md.set_block(0, BlockKind::Heading(9)),
5056 Err(Error::InvalidArgument)
5057 );
5058
5059 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5060 assert_eq!(
5061 xml.set_block(1, BlockKind::Heading(1)),
5062 Err(Error::UnsupportedFormat)
5063 );
5064 }
5065
5066 #[test]
5067 fn editor_toggle_block_container_round_trips() {
5068 let mut ed = Editor::new_str("a\n", Format::Djot).expect("editor");
5069
5070 let c = ed
5071 .toggle_block_container(0, 1, BlockContainerKind::BlockQuote)
5072 .expect("quote on");
5073 assert_eq!(ed.source_str().unwrap(), "> a\n");
5074 assert_eq!(&ed.source_str().unwrap()[c.new.clone()], "> a\n");
5075
5076 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
5077 .expect("quote off");
5078 assert_eq!(ed.source_str().unwrap(), "a\n");
5079 }
5080
5081 #[test]
5082 fn editor_toggle_block_container_nests_a_partial_selection() {
5083 let mut ed = Editor::new_str("> a\n>\n> b\n", Format::Djot).expect("editor");
5084
5085 ed.toggle_block_container(2, 3, BlockContainerKind::BlockQuote)
5088 .expect("nest");
5089 assert_eq!(ed.source_str().unwrap(), "> > a\n>\n> b\n");
5090
5091 ed.toggle_block_container(4, 5, BlockContainerKind::BlockQuote)
5093 .expect("peel");
5094 assert_eq!(ed.source_str().unwrap(), "> a\n>\n> b\n");
5095 }
5096
5097 #[test]
5098 fn editor_toggle_block_container_numbers_and_converts_lists() {
5099 let mut ed = Editor::new_str("a\n\nb\n", Format::Djot).expect("editor");
5100
5101 ed.toggle_block_container(0, 4, BlockContainerKind::OrderedList)
5103 .expect("ordered on");
5104 assert_eq!(ed.source_str().unwrap(), "1. a\n\n2. b\n");
5105
5106 ed.toggle_block_container(3, 9, BlockContainerKind::BulletList)
5108 .expect("convert");
5109 assert_eq!(ed.source_str().unwrap(), "- a\n\n- b\n");
5110 }
5111
5112 #[test]
5113 fn editor_toggle_block_container_rejects_unspellable_format() {
5114 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5115 assert_eq!(
5116 xml.toggle_block_container(3, 5, BlockContainerKind::BlockQuote),
5117 Err(Error::UnsupportedFormat)
5118 );
5119 }
5120
5121 #[test]
5122 fn editor_insert_link_wraps_and_repoints() {
5123 let mut ed = Editor::new_str("a word b\n", Format::Djot).expect("editor");
5124
5125 ed.insert_link(2, 6, "http://x.dev").expect("link");
5126 assert_eq!(ed.source_str().unwrap(), "a [word](http://x.dev) b\n");
5127
5128 ed.insert_link(3, 7, "http://y.dev").expect("re-point");
5130 assert_eq!(ed.source_str().unwrap(), "a [word](http://y.dev) b\n");
5131 }
5132
5133 #[test]
5134 fn editor_insert_link_repoints_an_autolink() {
5135 for format in [Format::Markdown, Format::Djot] {
5140 let mut ed = Editor::new_str("see <https://x.dev> ok\n", format).expect("editor");
5141 ed.insert_link(10, 10, "https://y.dev").expect("re-point");
5142 assert_eq!(ed.source_str().unwrap(), "see <https://y.dev> ok\n");
5143
5144 let nodes = ed.nodes().expect("nodes");
5146 let url = nodes
5147 .iter()
5148 .find(|n| n.kind == Kind::Url)
5149 .expect("still an autolink");
5150 assert_eq!(url.text.as_deref(), Some("https://y.dev"));
5151 assert!(!nodes.iter().any(|n| n.kind == Kind::Link));
5152 }
5153 }
5154
5155 #[test]
5156 fn editor_insert_link_escapes_the_destination() {
5157 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
5160 dj.insert_link(0, 1, "a)b").expect("link");
5161 assert_eq!(dj.source_str().unwrap(), "[w](a\\)b)\n");
5162
5163 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
5167 md.insert_link(0, 1, "a b").expect("link");
5168 assert_eq!(md.source_str().unwrap(), "[w](<a b>)\n");
5169
5170 let mut dj2 = Editor::new_str("w\n", Format::Djot).expect("editor");
5171 dj2.insert_link(0, 1, "a b").expect("link");
5172 assert_eq!(dj2.source_str().unwrap(), "[w](a b)\n");
5173 }
5174
5175 #[test]
5176 fn editor_insert_image_escapes_the_destination_per_format() {
5177 let mut md = Editor::new_str("w\n", Format::Markdown).expect("editor");
5180 md.insert_image(0, 1, "my cat.png").expect("image");
5181 assert_eq!(md.source_str().unwrap(), "\n");
5182
5183 let mut dj = Editor::new_str("w\n", Format::Djot).expect("editor");
5184 dj.insert_image(0, 1, "my cat.png").expect("image");
5185 assert_eq!(dj.source_str().unwrap(), "\n");
5186
5187 let mut paren = Editor::new_str("w\n", Format::Djot).expect("editor");
5189 paren.insert_image(0, 1, "a)b.png").expect("image");
5190 assert_eq!(paren.source_str().unwrap(), "b.png)\n");
5191 }
5192
5193 #[test]
5194 fn editor_insert_image_keeps_an_empty_alt_empty() {
5195 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5198 ed.insert_image(1, 1, "cat.png").expect("image");
5199 assert_eq!(ed.source_str().unwrap(), "ab\n");
5200 }
5201
5202 #[test]
5203 fn editor_insert_image_rejects_a_newline_destination() {
5204 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
5205 assert_eq!(
5206 ed.insert_image(0, 1, "a\nb.png"),
5207 Err(Error::InvalidArgument)
5208 );
5209
5210 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5211 assert_eq!(
5212 xml.insert_image(3, 5, "x.png"),
5213 Err(Error::UnsupportedFormat)
5214 );
5215 }
5216
5217 #[test]
5218 fn editor_insert_link_rejects_a_newline_destination() {
5219 let mut ed = Editor::new_str("w\n", Format::Djot).expect("editor");
5220 assert_eq!(ed.insert_link(0, 1, "a\nb"), Err(Error::InvalidArgument));
5221
5222 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5223 assert_eq!(xml.insert_link(3, 5, "u"), Err(Error::UnsupportedFormat));
5224 }
5225
5226 #[test]
5227 fn editor_insert_literal_keeps_typed_specials_literal() {
5228 for format in [Format::Markdown, Format::Djot] {
5229 let mut ed = Editor::new_str("z\n", format).expect("editor");
5230 ed.insert_literal(0, "*hi*").expect("literal");
5232
5233 let nodes = ed.nodes().expect("nodes");
5235 assert!(
5236 !nodes
5237 .iter()
5238 .any(|n| n.kind == Kind::Emph || n.kind == Kind::Strong)
5239 );
5240 let text: String = nodes
5241 .iter()
5242 .filter(|n| n.kind == Kind::Str)
5243 .filter_map(|n| n.text.clone())
5244 .collect();
5245 assert_eq!(text, "*hi*z");
5246 }
5247 }
5248
5249 #[test]
5250 fn editor_insert_literal_escapes_block_markers_only_at_line_start() {
5251 let mut ed = Editor::new_str("az\n", Format::Markdown).expect("editor");
5253 ed.insert_literal(1, "# ").expect("literal");
5254 assert_eq!(ed.source_str().unwrap(), "a# z\n");
5255
5256 let mut ed2 = Editor::new_str("z\n", Format::Markdown).expect("editor");
5258 ed2.insert_literal(0, "# ").expect("literal");
5259 assert_eq!(ed2.source_str().unwrap(), "\\# z\n");
5260 assert!(
5261 !ed2.nodes()
5262 .expect("nodes")
5263 .iter()
5264 .any(|n| n.kind == Kind::Heading)
5265 );
5266 }
5267
5268 #[test]
5269 fn editor_insert_literal_rejects_bad_offset_and_parse_only_format() {
5270 let mut ed = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5271 assert_eq!(ed.insert_literal(99, "x"), Err(Error::InvalidArgument));
5272
5273 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5274 assert_eq!(xml.insert_literal(3, "x"), Err(Error::UnsupportedFormat));
5275 }
5276
5277 #[test]
5278 fn editor_insert_line_break_splices_in_cell_br() {
5279 let mut ed =
5280 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
5281 ed.insert_line_break(3).expect("line break");
5283 assert_eq!(ed.source_str().unwrap(), "| a<br> | b |\n| --- | --- |\n");
5284 let nodes = ed.nodes().expect("nodes");
5286 assert!(nodes.iter().any(|n| n.kind == Kind::HardBreak));
5287 assert!(!nodes.iter().any(|n| n.kind == Kind::RawInline));
5288 }
5289
5290 #[test]
5291 fn editor_insert_line_break_rejects_off_cell_off_format_and_bad_offset() {
5292 let mut para = Editor::new_str("just text\n", Format::Markdown).expect("editor");
5294 assert_eq!(para.insert_line_break(3), Err(Error::NotFound));
5295
5296 let mut dj = Editor::new_str("| a | b |\n| --- | --- |\n", Format::Djot).expect("editor");
5298 assert_eq!(dj.insert_line_break(3), Err(Error::UnsupportedFormat));
5299
5300 let mut ed =
5302 Editor::new_str("| a | b |\n| --- | --- |\n", Format::Markdown).expect("editor");
5303 assert_eq!(ed.insert_line_break(9999), Err(Error::InvalidArgument));
5304 }
5305
5306 #[test]
5307 fn editor_insert_thematic_break_is_blank_separated_per_format() {
5308 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
5312 md.insert_thematic_break(0).expect("rule");
5313 assert_eq!(md.source_str().unwrap(), "a\n\n---\n");
5314 let nodes = md.nodes().expect("nodes");
5315 assert!(nodes.iter().any(|n| n.kind == Kind::ThematicBreak));
5316 assert!(!nodes.iter().any(|n| n.kind == Kind::Heading));
5317
5318 let mut dj = Editor::new_str("a\n", Format::Djot).expect("editor");
5321 dj.insert_thematic_break(0).expect("rule");
5322 assert_eq!(dj.source_str().unwrap(), "a\n\n* * *\n");
5323
5324 let mut xml = Editor::new_str("<a>hi</a>", Format::Xml).expect("editor");
5325 assert_eq!(xml.insert_thematic_break(3), Err(Error::UnsupportedFormat));
5326 }
5327
5328 #[test]
5329 fn editor_insert_table_writes_an_editable_table_after_the_block() {
5330 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
5331 md.insert_table(0, 1, 2).expect("table");
5332 assert_eq!(md.source_str().unwrap(), "a\n\n| | |\n| --- | --- |\n| | |\n");
5333 md.table_insert_row(4, true).expect("row");
5335 assert_eq!(
5336 md.source_str().unwrap(),
5337 "a\n\n| | |\n| --- | --- |\n| | |\n| | |\n"
5338 );
5339
5340 let mut dj = Editor::new_str("a\n", Format::Djot).expect("editor");
5341 dj.insert_table(0, 1, 2).expect("table");
5342 assert_eq!(dj.source_str().unwrap(), "a\n\n| | |\n|---|---|\n| | |\n");
5343
5344 let mut md = Editor::new_str("a\n", Format::Markdown).expect("editor");
5345 assert_eq!(md.insert_table(0, 0, 2), Err(Error::InvalidArgument));
5346 assert_eq!(md.insert_table(0, 1, 0), Err(Error::InvalidArgument));
5347 assert_eq!(md.source_str().unwrap(), "a\n");
5348
5349 let mut html = Editor::new_str("<p>ab</p>\n", Format::Html).expect("editor");
5350 assert_eq!(html.insert_table(4, 1, 1), Err(Error::UnsupportedFormat));
5351 assert!(!Format::Html.supports(Gesture::InsertTable));
5352 assert!(Format::Markdown.supports(Gesture::InsertTable));
5353 }
5354
5355 #[test]
5356 fn editor_split_block_keeps_both_halves_the_same_kind() {
5357 let mut item = Editor::new_str("- this is a list item\n", Format::Markdown).expect("editor");
5360 item.split_block(10).expect("split");
5361 assert_eq!(item.source_str().unwrap(), "- this is \n- a list item\n");
5362 let nodes = item.nodes().expect("nodes");
5363 assert_eq!(nodes.iter().filter(|n| n.kind == Kind::ListItem).count(), 2);
5364
5365 let mut tail = Editor::new_str("- a\n", Format::Markdown).expect("editor");
5367 tail.split_block(3).expect("split");
5368 assert_eq!(tail.source_str().unwrap(), "- a\n- \n");
5369
5370 let mut para = Editor::new_str("ab\n", Format::Markdown).expect("editor");
5372 para.split_block(1).expect("split");
5373 assert_eq!(para.source_str().unwrap(), "a\n\nb\n");
5374
5375 let mut table =
5377 Editor::new_str("| a | b |\n|---|---|\n| c | d |\n", Format::Markdown).expect("editor");
5378 assert_eq!(table.split_block(3), Err(Error::NotEditable));
5379
5380 let mut empty = Editor::new_str("", Format::Markdown).expect("editor");
5381 assert_eq!(empty.split_block(0), Err(Error::NotFound));
5382 }
5383
5384 #[test]
5385 fn editor_toggle_code_block_round_trips_and_measures_the_fence() {
5386 let mut ed = Editor::new_str("a\n", Format::Markdown).expect("editor");
5387 ed.toggle_code_block(0, 1, Some("zig")).expect("fence");
5388 assert_eq!(ed.source_str().unwrap(), "```zig\na\n```\n");
5389 let nodes = ed.nodes().expect("nodes");
5390 assert!(nodes.iter().any(|n| n.kind == Kind::CodeBlock));
5391
5392 ed.toggle_code_block(0, 0, None).expect("unfence");
5393 assert_eq!(ed.source_str().unwrap(), "a\n");
5394
5395 let mut runs = Editor::new_str("a ``` b\n", Format::Markdown).expect("editor");
5398 runs.toggle_code_block(0, 7, None).expect("fence");
5399 assert_eq!(runs.source_str().unwrap(), "````\na ``` b\n````\n");
5400 }
5401
5402 #[test]
5403 fn editor_toggle_code_block_refuses_inside_a_list_item() {
5404 let mut ed = Editor::new_str("- a\n- b\n", Format::Markdown).expect("editor");
5407 assert_eq!(ed.toggle_code_block(2, 3, None), Err(Error::NotEditable));
5408 assert_eq!(ed.source_str().unwrap(), "- a\n- b\n");
5409 }
5410
5411 #[test]
5412 fn editor_set_code_language_retags_clears_and_refuses() {
5413 let mut ed = Editor::new_str("```zig\na\n```\n", Format::Markdown).expect("editor");
5414 ed.set_code_language(0, Some("rust")).expect("retag");
5415 assert_eq!(ed.source_str().unwrap(), "```rust\na\n```\n");
5416
5417 ed.set_code_language(0, None).expect("clear");
5420 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
5421 ed.set_code_language(0, Some("")).expect("empty");
5422 assert_eq!(ed.source_str().unwrap(), "```\na\n```\n");
5423
5424 assert_eq!(
5427 ed.set_code_language(0, Some("a b")),
5428 Err(Error::InvalidArgument)
5429 );
5430 let mut dj = Editor::new_str("```\na\n```\n", Format::Djot).expect("editor");
5432 dj.set_code_language(0, Some("a b"))
5433 .expect("djot info string");
5434 assert_eq!(dj.source_str().unwrap(), "```a b\na\n```\n");
5435
5436 let mut para = Editor::new_str("x\n", Format::Markdown).expect("editor");
5437 assert_eq!(para.set_code_language(0, Some("zig")), Err(Error::NotFound));
5438 }
5439
5440 #[test]
5441 fn editor_task_checkbox_gestures() {
5442 let mut ed = Editor::new_str("- a\n", Format::Markdown).expect("editor");
5443
5444 ed.toggle_task_item(2).expect("add box");
5447 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
5448 assert!(
5449 ed.nodes()
5450 .unwrap()
5451 .iter()
5452 .any(|n| n.kind == Kind::TaskListItem)
5453 );
5454
5455 ed.set_task_checked(6, true).expect("tick");
5456 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
5457 ed.set_task_checked(6, true).expect("no-op");
5459 assert_eq!(ed.source_str().unwrap(), "- [x] a\n");
5460
5461 ed.toggle_task_checked(6).expect("flip");
5462 assert_eq!(ed.source_str().unwrap(), "- [ ] a\n");
5463
5464 ed.toggle_task_item(6).expect("remove box");
5465 assert_eq!(ed.source_str().unwrap(), "- a\n");
5466
5467 assert_eq!(ed.set_task_checked(2, true), Err(Error::NotEditable));
5470 let mut para = Editor::new_str("a\n", Format::Markdown).expect("editor");
5472 assert_eq!(para.toggle_task_item(0), Err(Error::NotFound));
5473 }
5474
5475 #[test]
5476 fn editor_insert_footnote_writes_both_halves_as_one_edit() {
5477 for format in [Format::Markdown, Format::Djot] {
5478 let mut ed = Editor::new_str("see\n", format).expect("editor");
5479 ed.insert_footnote(3, "a").expect("footnote");
5480 assert_eq!(ed.source_str().unwrap(), "see[^a]\n\n[^a]: \n");
5481
5482 let nodes = ed.nodes().expect("nodes");
5484 assert!(nodes.iter().any(|n| n.kind == Kind::FootnoteReference));
5485 assert!(nodes.iter().any(|n| n.kind == Kind::Footnote));
5486
5487 ed.undo().expect("undo");
5489 assert_eq!(ed.source_str().unwrap(), "see\n");
5490 }
5491 }
5492
5493 #[test]
5494 fn editor_insert_footnote_reuses_an_existing_definition() {
5495 let mut ed = Editor::new_str("see\n", Format::Markdown).expect("editor");
5496 ed.insert_footnote(3, "a").expect("first");
5497 ed.insert_footnote(7, "a").expect("second reference");
5498 assert_eq!(ed.source_str().unwrap(), "see[^a][^a]\n\n[^a]: \n");
5499 let defs = ed
5500 .nodes()
5501 .unwrap()
5502 .iter()
5503 .filter(|n| n.kind == Kind::Footnote)
5504 .count();
5505 assert_eq!(defs, 1);
5506
5507 assert_eq!(ed.insert_footnote(3, ""), Err(Error::InvalidArgument));
5508 assert_eq!(ed.insert_footnote(3, "a]b"), Err(Error::InvalidArgument));
5509 }
5510
5511 #[test]
5512 fn editor_undo_redo_round_trip() {
5513 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5514 ed.edit_range(5, 5, "!").expect("edit");
5515 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5516
5517 let change = ed.undo().expect("undo ok").expect("something to undo");
5518 assert_eq!(ed.source_str().unwrap(), "hello\n");
5519 assert_eq!(change.new.end, 5);
5520 assert!(ed.undo().expect("undo ok").is_none(), "history exhausted");
5521
5522 ed.redo().expect("redo ok").expect("something to redo");
5523 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5524 }
5525
5526 #[test]
5527 fn editor_coalesce_folds_a_run() {
5528 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5529 ed.edit_range(0, 0, "a").expect("edit");
5530 ed.edit_range(1, 1, "b").expect("edit");
5531 ed.coalesce_last_undo().expect("coalesce");
5532 assert_eq!(ed.source_str().unwrap(), "ab\n");
5533 ed.undo().expect("undo ok").expect("something to undo");
5535 assert_eq!(ed.source_str().unwrap(), "\n");
5536 assert!(ed.undo().expect("undo ok").is_none());
5537 }
5538
5539 #[test]
5540 fn editor_revision_bumps_per_successful_mutation() {
5541 let mut ed = Editor::new_str("x\n", Format::Markdown).expect("editor");
5542 assert_eq!(ed.revision(), 0);
5543 ed.edit_range(1, 1, "y").expect("edit");
5544 assert_eq!(ed.revision(), 1);
5545
5546 let mut xml = Editor::new_str("<a>ok</a>", Format::Xml).expect("editor");
5548 assert_eq!(xml.revision(), 0);
5549 assert!(xml.replace_content("0", "<b>").is_err());
5550 assert_eq!(xml.revision(), 0);
5551
5552 ed.undo().expect("undo ok").expect("something to undo");
5554 assert_eq!(ed.revision(), 2);
5555 ed.redo().expect("redo ok").expect("something to redo");
5556 assert_eq!(ed.revision(), 3);
5557 }
5558
5559 #[test]
5560 fn editor_dirty_range_tracks_and_clears() {
5561 let mut ed = Editor::new_str("abcdefgh\n", Format::Markdown).expect("editor");
5562 assert_eq!(ed.dirty_range(), None);
5564
5565 ed.edit_range(2, 2, "XY").expect("edit");
5567 assert_eq!(ed.dirty_range(), Some(2..4));
5568
5569 ed.edit_range(9, 9, "Z").expect("edit"); let d = ed.dirty_range().expect("dirty");
5573 assert!(
5574 d.start <= 2 && d.end >= 10,
5575 "range {d:?} must cover both edits"
5576 );
5577
5578 let rev = ed.revision();
5580 ed.clear_dirty();
5581 assert_eq!(ed.dirty_range(), None);
5582 assert_eq!(ed.revision(), rev);
5583
5584 ed.undo().expect("undo ok").expect("something to undo");
5586 assert!(ed.dirty_range().is_some());
5587 }
5588
5589 #[test]
5590 fn editor_caret_blob_follows_undo_and_redo() {
5591 let mut ed = Editor::new_str("hello\n", Format::Markdown).expect("editor");
5592 assert!(ed.caret_blob().unwrap().is_empty());
5593
5594 ed.set_caret_blob(b"before").expect("set caret");
5596 ed.edit_range(5, 5, "!").expect("edit");
5597 assert!(ed.caret_blob().unwrap().is_empty());
5599 ed.set_caret_blob(b"after").expect("set caret");
5600
5601 ed.undo().expect("undo ok").expect("something to undo");
5603 assert_eq!(ed.source_str().unwrap(), "hello\n");
5604 assert_eq!(ed.caret_blob().unwrap(), b"before");
5605
5606 ed.redo().expect("redo ok").expect("something to redo");
5608 assert_eq!(ed.source_str().unwrap(), "hello!\n");
5609 assert_eq!(ed.caret_blob().unwrap(), b"after");
5610 }
5611
5612 #[test]
5613 fn editor_coalesced_run_keeps_the_pre_run_caret() {
5614 let mut ed = Editor::new_str("\n", Format::Markdown).expect("editor");
5615 ed.set_caret_blob(b"c0").expect("set caret");
5616 ed.edit_range(0, 0, "a").expect("edit");
5617 ed.set_caret_blob(b"c1").expect("set caret");
5618 ed.edit_range(1, 1, "b").expect("edit");
5619 ed.coalesce_last_undo().expect("coalesce");
5620 ed.set_caret_blob(b"c2").expect("set caret");
5621
5622 ed.undo().expect("undo ok").expect("something to undo");
5624 assert_eq!(ed.source_str().unwrap(), "\n");
5625 assert_eq!(ed.caret_blob().unwrap(), b"c0");
5626 }
5627
5628 #[test]
5629 fn editor_renumber_ordered_lists_fixes_a_stale_sequence() {
5630 let mut ed = Editor::new_str("1. a\n2. x\n2. b\n3. c\n", Format::Markdown).expect("editor");
5631 ed.renumber_ordered_lists(0).expect("renumber ok");
5632 assert_eq!(ed.source_str().unwrap(), "1. a\n2. x\n3. b\n4. c\n");
5633 }
5634
5635 #[test]
5636 fn editor_renumber_ordered_lists_leaves_djot_prose_alone() {
5637 let src = "1. a\n 2. b\n2. c\n";
5640 let mut dj = Editor::new_str(src, Format::Djot).expect("editor");
5641 dj.renumber_ordered_lists(0).expect("renumber ok");
5642 assert_eq!(dj.source_str().unwrap(), src);
5643
5644 let mut md = Editor::new_str(src, Format::Markdown).expect("editor");
5645 md.renumber_ordered_lists(0).expect("renumber ok");
5646 assert_eq!(md.source_str().unwrap(), "1. a\n 1. b\n2. c\n");
5647 }
5648
5649 #[test]
5650 fn editor_renumber_ordered_lists_off_a_list_is_not_found() {
5651 let mut ed = Editor::new_str("a paragraph\n", Format::Markdown).expect("editor");
5652 assert!(matches!(ed.renumber_ordered_lists(2), Err(Error::NotFound)));
5653 }
5654
5655 #[test]
5656 fn editor_table_insert_row_and_set_alignment() {
5657 let src = "| a | b |\n| --- | --- |\n| 1 | 2 |\n";
5658 let mut ed = Editor::new_str(src, Format::Markdown).expect("editor");
5659 ed.table_insert_row(24, true).expect("insert row"); assert_eq!(
5661 ed.source_str().unwrap(),
5662 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| | |\n"
5663 );
5664 ed.table_set_alignment(6, Alignment::Center).expect("align"); assert!(ed.source_str().unwrap().contains("| --- | :---: |"));
5666 }
5667
5668 #[test]
5669 fn editor_table_edit_off_a_table_is_not_found() {
5670 let mut ed = Editor::new_str("nope\n", Format::Markdown).expect("editor");
5671 assert!(matches!(ed.table_delete_row(2), Err(Error::NotFound)));
5672 }
5673
5674 #[test]
5675 fn editor_set_block_converts_setext_heading() {
5676 let mut ed = Editor::new_str("Title\n=====\n\nbody\n", Format::Markdown).expect("editor");
5678 ed.set_block(0, BlockKind::Heading(1))
5679 .expect("setext to atx");
5680 assert_eq!(ed.source_str().unwrap(), "# Title\n\nbody\n");
5681 }
5682
5683 #[test]
5684 fn editor_unwrap_and_smart_delete() {
5685 let mut ed = Editor::new_str("<r><box><b/><c/></box></r>", Format::Xml).expect("editor");
5686 ed.unwrap_node("0.0").expect("unwrap"); assert_eq!(ed.source_str().expect("source"), "<r><b/><c/></r>");
5688
5689 let mut md = Editor::new_str("A\n\nB\n\nC\n", Format::Markdown).expect("editor");
5690 md.delete_smart("1").expect("delete_smart"); assert_eq!(md.source_str().expect("source"), "A\n\nC\n");
5692 }
5693
5694 #[test]
5695 fn editor_directives_require_the_extension_flag() {
5696 let src = ":::vis{.public}\nhi\n:::\n";
5697 let mut plain = Editor::new_str(src, Format::Markdown).expect("editor");
5700 assert_eq!(plain.query("directive").expect("query").len(), 0);
5701 let mut ext = Editor::new_ext(
5703 src.as_bytes(),
5704 Format::Markdown,
5705 MarkdownExtensions {
5706 directives: true,
5707 ..Default::default()
5708 },
5709 )
5710 .expect("editor");
5711 assert_eq!(ext.query("directive").expect("query").len(), 1);
5712 }
5713
5714 #[test]
5715 fn document_html_elements_make_embedded_img_queryable() {
5716 let src = "text <img src=\"a.png\" alt=\"x\"> more\n";
5717 let mut plain = Document::parse_str(src, Format::Markdown).expect("parse");
5719 assert_eq!(plain.query("image").expect("query").len(), 0);
5720 let mut ext = Document::parse_str_with(
5722 src,
5723 Format::Markdown,
5724 MarkdownExtensions {
5725 html_elements: true,
5726 ..Default::default()
5727 },
5728 )
5729 .expect("parse");
5730 let images = ext.query("image").expect("query");
5731 assert_eq!(images.len(), 1);
5732 assert_eq!(images[0].kind, Kind::Image);
5733 }
5734
5735 #[test]
5736 fn editor_filter_public_audience_view() {
5737 let src = "# Archive\n\n:::vis{.public}\nPublic.\n:::\n\n:::vis{.family}\nPrivate.\n:::\n";
5738 let mut ed = Editor::new_ext(
5739 src.as_bytes(),
5740 Format::Markdown,
5741 MarkdownExtensions {
5742 directives: true,
5743 ..Default::default()
5744 },
5745 )
5746 .expect("editor");
5747 ed.filter(
5749 "directive[name=vis]",
5750 Some("directive[class~=public]"),
5751 true,
5752 )
5753 .expect("filter");
5754 assert_eq!(ed.source_str().expect("source"), "# Archive\n\nPublic.\n");
5755 }
5756
5757 #[test]
5758 fn editor_filter_rejects_a_malformed_selector() {
5759 let mut ed = Editor::new_str("hi\n", Format::Markdown).expect("editor");
5760 assert_eq!(
5761 ed.filter("list >", None, false),
5762 Err(Error::InvalidArgument)
5763 );
5764 }
5765
5766 #[test]
5767 fn builder_builds_and_renders_a_document() {
5768 let mut b = Builder::new().expect("builder");
5769
5770 let title = b.add_text(TextKind::Str, "Title").unwrap();
5772 let heading = b.add_heading(1).unwrap();
5773 b.set_children(heading, &[title]).unwrap();
5774
5775 let hello = b.add_text(TextKind::Str, "hello ").unwrap();
5776 let world = b.add_text(TextKind::Str, "world").unwrap();
5777 let emph = b.add(VoidKind::Emph).unwrap();
5778 b.set_children(emph, &[world]).unwrap();
5779 let para = b.add(VoidKind::Para).unwrap();
5780 b.set_children(para, &[hello, emph]).unwrap();
5781
5782 let doc = b.add(VoidKind::Doc).unwrap();
5783 b.set_children(doc, &[heading, para]).unwrap();
5784
5785 let html = String::from_utf8(b.render_html(doc).unwrap()).unwrap();
5786 assert!(html.contains("<h1>Title</h1>"), "{html}");
5787 assert!(html.contains("<em>world</em>"), "{html}");
5788
5789 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5790 assert!(md.contains("# Title"), "{md}");
5791 assert!(md.contains("*world*"), "{md}");
5792
5793 let matches = b.query(doc, "heading").unwrap();
5794 assert_eq!(matches.len(), 1);
5795 assert_eq!(matches[0].kind, Kind::Heading);
5796
5797 let json = String::from_utf8(b.ast_json(doc).unwrap()).unwrap();
5798 assert!(json.contains("\"kind\": \"doc\""), "{json}");
5799 }
5800
5801 #[test]
5802 fn builder_element_with_attributes() {
5803 let mut b = Builder::new().expect("builder");
5804 let inner = b.add_text(TextKind::Str, "hi").unwrap();
5805 let el = b.add_element("section").unwrap();
5806 b.set_children(el, &[inner]).unwrap();
5807 b.set_attrs(el, &[("class", Some("note")), ("hidden", None)])
5808 .unwrap();
5809
5810 let html = String::from_utf8(b.render_html(el).unwrap()).unwrap();
5811 assert!(html.contains("<section"), "{html}");
5812 assert!(html.contains("class=\"note\""), "{html}");
5813 assert!(html.contains("hidden"), "{html}");
5814 }
5815
5816 #[test]
5817 fn builder_lists_round_trip_to_markdown() {
5818 let mut b = Builder::new().expect("builder");
5819
5820 let one_txt = b.add_text(TextKind::Str, "one").unwrap();
5822 let one_para = b.add(VoidKind::Para).unwrap();
5823 b.set_children(one_para, &[one_txt]).unwrap();
5824 let one = b.add(VoidKind::ListItem).unwrap();
5825 b.set_children(one, &[one_para]).unwrap();
5826
5827 let two_txt = b.add_text(TextKind::Str, "two").unwrap();
5828 let two_para = b.add(VoidKind::Para).unwrap();
5829 b.set_children(two_para, &[two_txt]).unwrap();
5830 let two = b.add(VoidKind::ListItem).unwrap();
5831 b.set_children(two, &[two_para]).unwrap();
5832
5833 let list = b
5834 .add_ordered_list(
5835 OrderedNumbering::Decimal,
5836 OrderedDelim::Period,
5837 true,
5838 Some(1),
5839 )
5840 .unwrap();
5841 b.set_children(list, &[one, two]).unwrap();
5842 let doc = b.add(VoidKind::Doc).unwrap();
5843 b.set_children(doc, &[list]).unwrap();
5844
5845 let md = String::from_utf8(b.serialize(doc, Format::Markdown).unwrap()).unwrap();
5846 assert!(md.contains("1. one"), "{md}");
5847 assert!(md.contains("2. two"), "{md}");
5848 }
5849
5850 #[test]
5851 fn builder_rejects_invalid_kind_and_id() {
5852 let b = Builder::new().expect("builder");
5853 let mut id = 0u32;
5857 let status = unsafe { ffi::twig_builder_add(b.raw.as_ptr(), 2, &mut id) };
5858 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5859
5860 let mut ptr = std::ptr::null();
5862 let mut len = 0usize;
5863 let status =
5864 unsafe { ffi::twig_builder_render_html(b.raw.as_ptr(), 4242, &mut ptr, &mut len) };
5865 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5866 }
5867
5868 fn all_gestures() -> Vec<Gesture> {
5872 let inline = [
5873 InlineKind::Strong,
5874 InlineKind::Emph,
5875 InlineKind::Verbatim,
5876 InlineKind::Mark,
5877 InlineKind::Superscript,
5878 InlineKind::Subscript,
5879 InlineKind::Insert,
5880 InlineKind::Delete,
5881 ];
5882 let mut all: Vec<Gesture> = Vec::new();
5883 for k in inline {
5884 all.push(Gesture::WrapRange(k));
5885 all.push(Gesture::ToggleInline(k));
5886 }
5887 for k in [
5888 BlockContainerKind::BlockQuote,
5889 BlockContainerKind::BulletList,
5890 BlockContainerKind::OrderedList,
5891 ] {
5892 all.push(Gesture::ToggleBlockContainer(k));
5893 }
5894 all.extend([
5895 Gesture::SetMarkColor,
5896 Gesture::SetBlock,
5897 Gesture::InsertThematicBreak,
5898 Gesture::ToggleCodeBlock,
5899 Gesture::SetCodeLanguage,
5900 Gesture::ToggleTaskItem,
5901 Gesture::SetTaskChecked,
5902 Gesture::ToggleTaskChecked,
5903 Gesture::InsertLink,
5904 Gesture::InsertImage,
5905 Gesture::InsertFootnote,
5906 Gesture::InsertLiteral,
5907 Gesture::InsertLineBreak,
5908 Gesture::SplitBlock,
5909 Gesture::RenumberOrderedLists,
5910 Gesture::TableInsertRow,
5911 Gesture::TableDeleteRow,
5912 Gesture::TableInsertColumn,
5913 Gesture::TableDeleteColumn,
5914 Gesture::TableSetAlignment,
5915 Gesture::TableMoveRow,
5916 Gesture::TableMoveColumn,
5917 Gesture::InsertTable,
5918 ]);
5919 all
5920 }
5921
5922 #[test]
5923 fn the_wire_space_ends_where_the_sweep_does() {
5924 let mut codes: Vec<c_int> = all_gestures().iter().map(|g| g.to_c().0).collect();
5930 codes.sort_unstable();
5931 codes.dedup();
5932 assert_eq!(codes, (0..=25).collect::<Vec<c_int>>());
5933
5934 let mut supported = -1;
5935 for code in &codes {
5936 let status = unsafe {
5937 ffi::twig_format_supports(
5938 ffi::TwigFormat::from(Format::Markdown) as c_int,
5939 *code,
5940 0,
5941 &mut supported,
5942 )
5943 };
5944 assert_eq!(Error::from_status(status), Ok(()), "code {code} did not decode");
5945 }
5946 let status = unsafe {
5948 ffi::twig_format_supports(
5949 ffi::TwigFormat::from(Format::Markdown) as c_int,
5950 26,
5951 0,
5952 &mut supported,
5953 )
5954 };
5955 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
5956 }
5957
5958 #[test]
5959 fn supports_answers_per_gesture_where_authorable_cannot() {
5960 assert!(Format::Html.is_authorable());
5965 assert!(Format::Html.supports(Gesture::ToggleInline(InlineKind::Strong)));
5966 assert!(Format::Html.supports(Gesture::SetBlock));
5967 assert!(Format::Html.supports(Gesture::InsertLiteral));
5968 assert!(Format::Html.supports(Gesture::ToggleBlockContainer(
5969 BlockContainerKind::BlockQuote
5970 )));
5971 assert!(Format::Html.supports(Gesture::ToggleCodeBlock));
5972 assert!(Format::Html.supports(Gesture::InsertLink));
5973 assert!(!Format::Html.supports(Gesture::ToggleTaskItem));
5974 assert!(!Format::Html.supports(Gesture::InsertFootnote));
5975 assert!(!Format::Html.supports(Gesture::TableInsertRow));
5979 assert!(!Format::Html.supports(Gesture::TableSetAlignment));
5980 assert!(!Format::Html.supports(Gesture::SplitBlock));
5981 assert!(!Format::Html.supports(Gesture::RenumberOrderedLists));
5982 assert!(Format::Markdown.supports(Gesture::TableInsertRow));
5983 assert!(Format::Djot.supports(Gesture::SplitBlock));
5984
5985 for fmt in [Format::Xml] {
5988 assert!(!fmt.is_authorable());
5989 for g in all_gestures() {
5990 assert!(!fmt.supports(g), "{fmt:?} claims to spell {g:?}");
5991 }
5992 }
5993 assert!(Format::Asciidoc.is_authorable());
5997 assert!(Format::Asciidoc.supports(Gesture::SetBlock));
5998 assert!(Format::Asciidoc.supports(Gesture::ToggleInline(InlineKind::Mark)));
5999 assert!(Format::Asciidoc.supports(Gesture::InsertLink));
6000 assert!(!Format::Asciidoc.supports(Gesture::InsertFootnote));
6001 assert!(!Format::Asciidoc.supports(Gesture::TableInsertRow));
6002
6003 assert!(Format::Djot.supports(Gesture::ToggleInline(InlineKind::Mark)));
6006 assert!(!Format::Markdown.supports(Gesture::ToggleInline(InlineKind::Mark)));
6007 assert!(Format::Markdown.supports(Gesture::InsertLineBreak));
6008 assert!(!Format::Djot.supports(Gesture::InsertLineBreak));
6009 }
6010
6011 #[test]
6012 fn supports_agrees_with_what_the_editor_then_does() {
6013 for fmt in [Format::Djot, Format::Markdown, Format::Html] {
6018 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
6019 let claimed = fmt.supports(Gesture::ToggleInline(InlineKind::Mark));
6020 let observed = ed.toggle_inline(0, 2, InlineKind::Mark);
6021 assert_eq!(
6022 claimed,
6023 !matches!(observed, Err(Error::UnsupportedFormat)),
6024 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
6025 );
6026
6027 let mut ed = Editor::new_str("ab\n", fmt).expect("editor");
6028 let claimed = fmt.supports(Gesture::SetBlock);
6029 let observed = ed.set_block(0, BlockKind::Heading(1));
6030 assert_eq!(
6031 claimed,
6032 !matches!(observed, Err(Error::UnsupportedFormat)),
6033 "{fmt:?}: supports said {claimed}, gesture said {observed:?}",
6034 );
6035 }
6036
6037 let src = "<table><tr><td>a</td></tr></table>";
6041 let mut ed = Editor::new_str(src, Format::Html).expect("editor");
6042 assert!(!Format::Html.supports(Gesture::TableInsertRow));
6043 assert_eq!(ed.table_insert_row(15, true), Err(Error::UnsupportedFormat));
6044 assert_eq!(ed.renumber_ordered_lists(15), Err(Error::UnsupportedFormat));
6045 assert!(matches!(ed.split_block(15), Err(Error::UnsupportedFormat)));
6046 assert_eq!(ed.source().expect("source"), src.as_bytes());
6047 }
6048
6049 #[test]
6050 fn supports_rides_the_gestures_own_kind_space() {
6051 let (g, k) = Gesture::ToggleBlockContainer(BlockContainerKind::BulletList).to_c();
6056 assert_eq!((g, k), (3, 1));
6057 let (g, k) = Gesture::ToggleInline(InlineKind::Emph).to_c();
6058 assert_eq!((g, k), (1, 1));
6059 assert_eq!(Gesture::InsertLink.to_c(), (10, 0));
6062
6063 let mut out: c_int = 0;
6065 let status = unsafe {
6066 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 10, 3, &mut out)
6067 };
6068 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
6069 let status = unsafe {
6070 ffi::twig_format_supports(ffi::TwigFormat::Markdown as c_int, 9999, 0, &mut out)
6071 };
6072 assert_eq!(Error::from_status(status), Err(Error::InvalidArgument));
6073 }
6074}