Skip to main content

mq_markdown/
node.rs

1use crate::node::attr_value::{
2    AttrValue,
3    attr_keys::{self, CHILDREN},
4};
5use itertools::Itertools;
6use markdown::mdast::{self};
7use smol_str::SmolStr;
8use std::{
9    borrow::Cow,
10    fmt::{self, Display},
11};
12
13type ColorPair<'a> = (Cow<'a, str>, Cow<'a, str>);
14
15pub mod attr_value;
16
17/// Color theme for rendering markdown nodes with optional ANSI escape codes.
18///
19/// Each field is a tuple of `(prefix, suffix)` ANSI escape code strings that
20/// wrap the corresponding markdown element during colored rendering.
21#[derive(Debug, Clone, Default)]
22pub struct ColorTheme<'a> {
23    pub heading: ColorPair<'a>,
24    pub code: ColorPair<'a>,
25    pub code_inline: ColorPair<'a>,
26    pub emphasis: ColorPair<'a>,
27    pub strong: ColorPair<'a>,
28    pub link: ColorPair<'a>,
29    pub link_url: ColorPair<'a>,
30    pub image: ColorPair<'a>,
31    pub blockquote_marker: ColorPair<'a>,
32    pub delete: ColorPair<'a>,
33    pub horizontal_rule: ColorPair<'a>,
34    pub html: ColorPair<'a>,
35    pub frontmatter: ColorPair<'a>,
36    pub list_marker: ColorPair<'a>,
37    pub table_separator: ColorPair<'a>,
38    pub math: ColorPair<'a>,
39}
40
41const EMPTY: Cow<'_, str> = Cow::Borrowed("");
42#[cfg(feature = "color")]
43const RESET: Cow<'_, str> = Cow::Borrowed("\x1b[0m");
44
45impl ColorTheme<'_> {
46    pub const PLAIN: ColorTheme<'static> = ColorTheme {
47        heading: (EMPTY, EMPTY),
48        code: (EMPTY, EMPTY),
49        code_inline: (EMPTY, EMPTY),
50        emphasis: (EMPTY, EMPTY),
51        strong: (EMPTY, EMPTY),
52        link: (EMPTY, EMPTY),
53        link_url: (EMPTY, EMPTY),
54        image: (EMPTY, EMPTY),
55        blockquote_marker: (EMPTY, EMPTY),
56        delete: (EMPTY, EMPTY),
57        horizontal_rule: (EMPTY, EMPTY),
58        html: (EMPTY, EMPTY),
59        frontmatter: (EMPTY, EMPTY),
60        list_marker: (EMPTY, EMPTY),
61        table_separator: (EMPTY, EMPTY),
62        math: (EMPTY, EMPTY),
63    };
64
65    #[cfg(feature = "color")]
66    pub const COLORED: ColorTheme<'static> = ColorTheme {
67        heading: (Cow::Borrowed("\x1b[1m\x1b[36m"), RESET),
68        code: (Cow::Borrowed("\x1b[32m"), RESET),
69        code_inline: (Cow::Borrowed("\x1b[32m"), RESET),
70        emphasis: (Cow::Borrowed("\x1b[3m\x1b[33m"), RESET),
71        strong: (Cow::Borrowed("\x1b[1m"), RESET),
72        link: (Cow::Borrowed("\x1b[4m\x1b[34m"), RESET),
73        link_url: (Cow::Borrowed("\x1b[34m"), RESET),
74        image: (Cow::Borrowed("\x1b[35m"), RESET),
75        blockquote_marker: (Cow::Borrowed("\x1b[2m"), RESET),
76        delete: (Cow::Borrowed("\x1b[31m\x1b[2m"), RESET),
77        horizontal_rule: (Cow::Borrowed("\x1b[2m"), RESET),
78        html: (Cow::Borrowed("\x1b[2m"), RESET),
79        frontmatter: (Cow::Borrowed("\x1b[2m"), RESET),
80        list_marker: (Cow::Borrowed("\x1b[33m"), RESET),
81        table_separator: (Cow::Borrowed("\x1b[2m"), RESET),
82        math: (Cow::Borrowed("\x1b[32m"), RESET),
83    };
84
85    /// Creates a color theme from the `MQ_COLORS` environment variable.
86    #[cfg(feature = "color")]
87    pub fn from_env() -> ColorTheme<'static> {
88        match std::env::var("MQ_COLORS") {
89            Ok(v) if !v.is_empty() => ColorTheme::parse_colors(&v),
90            _ => Self::COLORED,
91        }
92    }
93
94    /// Parses a color configuration string into a `ColorTheme`.
95    ///
96    /// The format is `key=SGR:key=SGR:...` where each key corresponds to a
97    /// markdown element and the value is a semicolon-separated list of SGR
98    /// parameters. Unspecified keys use the default colored theme values.
99    /// Invalid entries are silently ignored.
100    #[cfg(feature = "color")]
101    pub fn parse_colors(spec: &str) -> ColorTheme<'static> {
102        let mut theme = Self::COLORED;
103
104        for entry in spec.split(':') {
105            let Some((key, sgr)) = entry.split_once('=') else {
106                continue;
107            };
108
109            if !Self::is_valid_sgr(sgr) {
110                continue;
111            }
112
113            let prefix = Cow::Owned(format!("\x1b[{}m", sgr));
114            let pair = (prefix, RESET);
115
116            match key {
117                "heading" => theme.heading = pair,
118                "code" => theme.code = pair,
119                "code_inline" => theme.code_inline = pair,
120                "emphasis" => theme.emphasis = pair,
121                "strong" => theme.strong = pair,
122                "link" => theme.link = pair,
123                "link_url" => theme.link_url = pair,
124                "image" => theme.image = pair,
125                "blockquote" => theme.blockquote_marker = pair,
126                "delete" => theme.delete = pair,
127                "hr" => theme.horizontal_rule = pair,
128                "html" => theme.html = pair,
129                "frontmatter" => theme.frontmatter = pair,
130                "list" => theme.list_marker = pair,
131                "table" => theme.table_separator = pair,
132                "math" => theme.math = pair,
133                _ => {}
134            }
135        }
136
137        theme
138    }
139
140    /// Validates that a string contains only valid SGR parameters
141    /// (semicolon-separated numbers).
142    #[cfg(feature = "color")]
143    fn is_valid_sgr(sgr: &str) -> bool {
144        !sgr.is_empty() && sgr.split(';').all(|part| part.parse::<u8>().is_ok())
145    }
146}
147
148type Level = u8;
149
150pub const EMPTY_NODE: Node = Node::Text(Text {
151    value: String::new(),
152    position: None,
153});
154
155#[derive(Debug, Clone, Default, PartialEq)]
156pub struct RenderOptions {
157    pub list_style: ListStyle,
158    pub link_url_style: UrlSurroundStyle,
159    pub link_title_style: TitleSurroundStyle,
160}
161
162#[derive(Debug, Clone, Default, PartialEq)]
163pub enum ListStyle {
164    #[default]
165    Dash,
166    Plus,
167    Star,
168}
169
170impl Display for ListStyle {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            ListStyle::Dash => write!(f, "-"),
174            ListStyle::Plus => write!(f, "+"),
175            ListStyle::Star => write!(f, "*"),
176        }
177    }
178}
179
180#[derive(Debug, Clone, PartialEq, Default)]
181#[cfg_attr(
182    feature = "json",
183    derive(serde::Serialize, serde::Deserialize),
184    serde(rename_all = "camelCase")
185)]
186pub struct Url(String);
187
188#[derive(Debug, Clone, PartialEq, Default)]
189pub enum UrlSurroundStyle {
190    #[default]
191    None,
192    Angle,
193}
194
195impl Url {
196    pub fn new(value: String) -> Self {
197        Self(value)
198    }
199
200    pub fn as_str(&self) -> &str {
201        &self.0
202    }
203
204    pub fn to_string_with(&self, options: &RenderOptions) -> Cow<'_, str> {
205        match options.link_url_style {
206            UrlSurroundStyle::None if self.0.is_empty() => Cow::Borrowed(""),
207            UrlSurroundStyle::None => Cow::Borrowed(&self.0),
208            UrlSurroundStyle::Angle => Cow::Owned(format!("<{}>", self.0)),
209        }
210    }
211}
212
213#[derive(Debug, Clone, PartialEq, Default)]
214pub enum TitleSurroundStyle {
215    #[default]
216    Double,
217    Single,
218    Paren,
219}
220
221#[derive(Debug, Clone, PartialEq)]
222#[cfg_attr(
223    feature = "json",
224    derive(serde::Serialize, serde::Deserialize),
225    serde(rename_all = "camelCase")
226)]
227pub struct Title(String);
228
229impl Display for Title {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        write!(f, "{}", self.0)
232    }
233}
234
235impl Title {
236    pub fn new(value: String) -> Self {
237        Self(value)
238    }
239
240    pub fn to_value(&self) -> String {
241        self.0.clone()
242    }
243
244    pub fn to_string_with(&self, options: &RenderOptions) -> Cow<'_, str> {
245        match options.link_title_style {
246            TitleSurroundStyle::Double => Cow::Owned(format!("\"{}\"", self)),
247            TitleSurroundStyle::Single => Cow::Owned(format!("'{}'", self)),
248            TitleSurroundStyle::Paren => Cow::Owned(format!("({})", self)),
249        }
250    }
251}
252
253#[derive(Debug, Clone, PartialEq)]
254#[cfg_attr(
255    feature = "json",
256    derive(serde::Serialize, serde::Deserialize),
257    serde(rename_all = "camelCase")
258)]
259pub enum TableAlignKind {
260    Left,
261    Right,
262    Center,
263    None,
264}
265
266impl From<mdast::AlignKind> for TableAlignKind {
267    fn from(value: mdast::AlignKind) -> Self {
268        match value {
269            mdast::AlignKind::Left => Self::Left,
270            mdast::AlignKind::Right => Self::Right,
271            mdast::AlignKind::Center => Self::Center,
272            mdast::AlignKind::None => Self::None,
273        }
274    }
275}
276
277impl From<&str> for TableAlignKind {
278    fn from(value: &str) -> Self {
279        match value {
280            "left" | ":---" => Self::Left,
281            "right" | "---:" => Self::Right,
282            "center" | ":---:" => Self::Center,
283            "---" => Self::None,
284            _ => Self::None,
285        }
286    }
287}
288
289impl Display for TableAlignKind {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        match self {
292            TableAlignKind::Left => write!(f, ":---"),
293            TableAlignKind::Right => write!(f, "---:"),
294            TableAlignKind::Center => write!(f, ":---:"),
295            TableAlignKind::None => write!(f, "---"),
296        }
297    }
298}
299
300#[derive(Debug, Clone, PartialEq, Default)]
301#[cfg_attr(
302    feature = "json",
303    derive(serde::Serialize, serde::Deserialize),
304    serde(rename_all = "camelCase", tag = "type")
305)]
306pub struct List {
307    pub values: Vec<Node>,
308    pub index: usize,
309    pub level: Level,
310    pub ordered: bool,
311    pub checked: Option<bool>,
312    pub position: Option<Position>,
313}
314
315#[derive(Debug, Clone, PartialEq)]
316#[cfg_attr(
317    feature = "json",
318    derive(serde::Serialize, serde::Deserialize),
319    serde(rename_all = "camelCase", tag = "type")
320)]
321pub struct TableCell {
322    pub values: Vec<Node>,
323    pub column: usize,
324    pub row: usize,
325    pub position: Option<Position>,
326}
327
328#[derive(Debug, Clone, PartialEq)]
329#[cfg_attr(
330    feature = "json",
331    derive(serde::Serialize, serde::Deserialize),
332    serde(rename_all = "camelCase", tag = "type")
333)]
334pub struct TableRow {
335    pub values: Vec<Node>,
336    pub position: Option<Position>,
337}
338
339#[derive(Debug, Clone, PartialEq)]
340#[cfg_attr(
341    feature = "json",
342    derive(serde::Serialize, serde::Deserialize),
343    serde(rename_all = "camelCase", tag = "type")
344)]
345pub struct TableAlign {
346    pub align: Vec<TableAlignKind>,
347    pub position: Option<Position>,
348}
349
350#[derive(Debug, Clone, PartialEq)]
351#[cfg_attr(
352    feature = "json",
353    derive(serde::Serialize, serde::Deserialize),
354    serde(rename_all = "camelCase", tag = "type")
355)]
356pub struct Fragment {
357    pub values: Vec<Node>,
358}
359
360#[derive(Debug, Clone, PartialEq, Default)]
361#[cfg_attr(
362    feature = "json",
363    derive(serde::Serialize, serde::Deserialize),
364    serde(rename_all = "camelCase", tag = "type")
365)]
366pub struct Code {
367    pub value: String,
368    pub lang: Option<String>,
369    pub position: Option<Position>,
370    pub meta: Option<String>,
371    pub fence: bool,
372}
373
374#[derive(Debug, Clone, PartialEq)]
375#[cfg_attr(
376    feature = "json",
377    derive(serde::Serialize, serde::Deserialize),
378    serde(rename_all = "camelCase", tag = "type")
379)]
380pub struct Image {
381    pub alt: String,
382    pub url: String,
383    pub title: Option<String>,
384    pub position: Option<Position>,
385}
386
387#[derive(Debug, Clone, PartialEq, Default)]
388#[cfg_attr(
389    feature = "json",
390    derive(serde::Serialize, serde::Deserialize),
391    serde(rename_all = "camelCase", tag = "type")
392)]
393pub struct ImageRef {
394    pub alt: String,
395    pub ident: String,
396    pub label: Option<String>,
397    pub position: Option<Position>,
398}
399#[derive(Debug, Clone, PartialEq)]
400#[cfg_attr(
401    feature = "json",
402    derive(serde::Serialize, serde::Deserialize),
403    serde(rename_all = "camelCase", tag = "type")
404)]
405pub struct Link {
406    pub url: Url,
407    pub title: Option<Title>,
408    pub values: Vec<Node>,
409    pub position: Option<Position>,
410}
411
412/// An Obsidian-style callout (admonition): `> [!TYPE]` or `> [!TYPE] title`.
413#[cfg(feature = "callout")]
414#[derive(Debug, Clone, PartialEq, Default)]
415#[cfg_attr(
416    feature = "json",
417    derive(serde::Serialize, serde::Deserialize),
418    serde(rename_all = "camelCase", tag = "type")
419)]
420pub struct Callout {
421    /// The callout type in uppercase (e.g. `"NOTE"`, `"WARNING"`, `"TIP"`).
422    pub kind: String,
423    /// Optional custom title after the `[!TYPE]` marker.
424    pub title: Option<String>,
425    /// Body content nodes (the lines after the header).
426    pub values: Vec<Node>,
427    pub position: Option<Position>,
428}
429
430/// An Obsidian-style file embed: `![[target]]` or `![[target|display]]`.
431#[cfg(feature = "embed")]
432#[derive(Debug, Clone, PartialEq, Default)]
433#[cfg_attr(
434    feature = "json",
435    derive(serde::Serialize, serde::Deserialize),
436    serde(rename_all = "camelCase", tag = "type")
437)]
438pub struct Embed {
439    /// The target file or note name (e.g. `"image.png"`, `"note.md"`).
440    pub target: String,
441    /// Optional display hint after `|` (size for images, heading for notes).
442    pub display: Option<String>,
443    pub position: Option<Position>,
444}
445
446/// An Obsidian-style wikilink: `[[target]]` or `[[target|text]]`.
447#[cfg(feature = "wikilink")]
448#[derive(Debug, Clone, PartialEq, Default)]
449#[cfg_attr(
450    feature = "json",
451    derive(serde::Serialize, serde::Deserialize),
452    serde(rename_all = "camelCase", tag = "type")
453)]
454pub struct WikiLink {
455    /// The target page or file name (e.g. `"Three laws of motion"`).
456    pub target: String,
457    /// Optional display text (the part after `|`).
458    pub text: Option<String>,
459    pub position: Option<Position>,
460}
461
462#[derive(Debug, Clone, PartialEq, Default)]
463#[cfg_attr(
464    feature = "json",
465    derive(serde::Serialize, serde::Deserialize),
466    serde(rename_all = "camelCase", tag = "type")
467)]
468pub struct FootnoteRef {
469    pub ident: String,
470    pub label: Option<String>,
471    pub position: Option<Position>,
472}
473
474#[derive(Debug, Clone, PartialEq, Default)]
475#[cfg_attr(
476    feature = "json",
477    derive(serde::Serialize, serde::Deserialize),
478    serde(rename_all = "camelCase", tag = "type")
479)]
480pub struct Footnote {
481    pub ident: String,
482    pub values: Vec<Node>,
483    pub position: Option<Position>,
484}
485
486#[derive(Debug, Clone, PartialEq, Default)]
487#[cfg_attr(
488    feature = "json",
489    derive(serde::Serialize, serde::Deserialize),
490    serde(rename_all = "camelCase", tag = "type")
491)]
492pub struct LinkRef {
493    pub ident: String,
494    pub label: Option<String>,
495    pub values: Vec<Node>,
496    pub position: Option<Position>,
497}
498
499#[derive(Debug, Clone, PartialEq, Default)]
500#[cfg_attr(
501    feature = "json",
502    derive(serde::Serialize, serde::Deserialize),
503    serde(rename_all = "camelCase", tag = "type")
504)]
505pub struct Heading {
506    pub depth: u8,
507    pub values: Vec<Node>,
508    pub position: Option<Position>,
509}
510
511#[derive(Debug, Clone, PartialEq, Default)]
512#[cfg_attr(
513    feature = "json",
514    derive(serde::Serialize, serde::Deserialize),
515    serde(rename_all = "camelCase", tag = "type")
516)]
517pub struct Definition {
518    pub position: Option<Position>,
519    pub url: Url,
520    pub title: Option<Title>,
521    pub ident: String,
522    pub label: Option<String>,
523}
524#[derive(Debug, Clone, PartialEq)]
525#[cfg_attr(
526    feature = "json",
527    derive(serde::Serialize, serde::Deserialize),
528    serde(rename_all = "camelCase", tag = "type")
529)]
530pub struct Text {
531    pub value: String,
532    pub position: Option<Position>,
533}
534
535#[derive(Debug, Clone, PartialEq)]
536#[cfg_attr(
537    feature = "json",
538    derive(serde::Serialize, serde::Deserialize),
539    serde(rename_all = "camelCase", tag = "type")
540)]
541pub struct Html {
542    pub value: String,
543    pub position: Option<Position>,
544}
545
546#[derive(Debug, Clone, PartialEq)]
547#[cfg_attr(
548    feature = "json",
549    derive(serde::Serialize, serde::Deserialize),
550    serde(rename_all = "camelCase", tag = "type")
551)]
552pub struct Toml {
553    pub value: String,
554    pub position: Option<Position>,
555}
556
557#[derive(Debug, Clone, PartialEq)]
558#[cfg_attr(
559    feature = "json",
560    derive(serde::Serialize, serde::Deserialize),
561    serde(rename_all = "camelCase", tag = "type")
562)]
563pub struct Yaml {
564    pub value: String,
565    pub position: Option<Position>,
566}
567
568#[derive(Debug, Clone, PartialEq)]
569#[cfg_attr(
570    feature = "json",
571    derive(serde::Serialize, serde::Deserialize),
572    serde(rename_all = "camelCase", tag = "type")
573)]
574pub struct CodeInline {
575    pub value: SmolStr,
576    pub position: Option<Position>,
577}
578
579#[derive(Debug, Clone, PartialEq)]
580#[cfg_attr(
581    feature = "json",
582    derive(serde::Serialize, serde::Deserialize),
583    serde(rename_all = "camelCase", tag = "type")
584)]
585pub struct MathInline {
586    pub value: SmolStr,
587    pub position: Option<Position>,
588}
589
590#[derive(Debug, Clone, PartialEq)]
591#[cfg_attr(
592    feature = "json",
593    derive(serde::Serialize, serde::Deserialize),
594    serde(rename_all = "camelCase", tag = "type")
595)]
596pub struct Math {
597    pub value: String,
598    pub position: Option<Position>,
599}
600
601#[derive(Debug, Clone, PartialEq)]
602#[cfg_attr(
603    feature = "json",
604    derive(serde::Serialize, serde::Deserialize),
605    serde(rename_all = "camelCase", tag = "type")
606)]
607pub struct MdxFlowExpression {
608    pub value: SmolStr,
609    pub position: Option<Position>,
610}
611
612#[derive(Debug, Clone, PartialEq)]
613#[cfg_attr(
614    feature = "json",
615    derive(serde::Serialize, serde::Deserialize),
616    serde(rename_all = "camelCase", tag = "type")
617)]
618pub struct MdxJsxFlowElement {
619    pub children: Vec<Node>,
620    pub position: Option<Position>,
621    pub name: Option<String>,
622    pub attributes: Vec<MdxAttributeContent>,
623}
624
625#[derive(Debug, Clone, PartialEq)]
626#[cfg_attr(
627    feature = "json",
628    derive(serde::Serialize, serde::Deserialize),
629    serde(rename_all = "camelCase", tag = "type")
630)]
631pub enum MdxAttributeContent {
632    Expression(SmolStr),
633    Property(MdxJsxAttribute),
634}
635
636#[derive(Debug, Clone, PartialEq)]
637#[cfg_attr(
638    feature = "json",
639    derive(serde::Serialize, serde::Deserialize),
640    serde(rename_all = "camelCase", tag = "type")
641)]
642pub struct MdxJsxAttribute {
643    pub name: SmolStr,
644    pub value: Option<MdxAttributeValue>,
645}
646
647#[derive(Debug, Clone, PartialEq)]
648#[cfg_attr(
649    feature = "json",
650    derive(serde::Serialize, serde::Deserialize),
651    serde(rename_all = "camelCase", tag = "type")
652)]
653pub enum MdxAttributeValue {
654    Expression(SmolStr),
655    Literal(SmolStr),
656}
657
658#[derive(Debug, Clone, PartialEq)]
659#[cfg_attr(
660    feature = "json",
661    derive(serde::Serialize, serde::Deserialize),
662    serde(rename_all = "camelCase", tag = "type")
663)]
664pub struct MdxJsxTextElement {
665    pub children: Vec<Node>,
666    pub position: Option<Position>,
667    pub name: Option<SmolStr>,
668    pub attributes: Vec<MdxAttributeContent>,
669}
670
671#[derive(Debug, Clone, PartialEq)]
672#[cfg_attr(
673    feature = "json",
674    derive(serde::Serialize, serde::Deserialize),
675    serde(rename_all = "camelCase", tag = "type")
676)]
677pub struct MdxTextExpression {
678    pub value: SmolStr,
679    pub position: Option<Position>,
680}
681
682#[derive(Debug, Clone, PartialEq)]
683#[cfg_attr(
684    feature = "json",
685    derive(serde::Serialize, serde::Deserialize),
686    serde(rename_all = "camelCase", tag = "type")
687)]
688pub struct MdxJsEsm {
689    pub value: SmolStr,
690    pub position: Option<Position>,
691}
692
693#[derive(Debug, Clone, PartialEq)]
694#[cfg_attr(
695    feature = "json",
696    derive(serde::Serialize, serde::Deserialize),
697    serde(rename_all = "camelCase", tag = "type")
698)]
699pub struct Blockquote {
700    pub values: Vec<Node>,
701    pub position: Option<Position>,
702}
703
704#[derive(Debug, Clone, PartialEq)]
705#[cfg_attr(
706    feature = "json",
707    derive(serde::Serialize, serde::Deserialize),
708    serde(rename_all = "camelCase", tag = "type")
709)]
710pub struct Delete {
711    pub values: Vec<Node>,
712    pub position: Option<Position>,
713}
714
715#[derive(Debug, Clone, PartialEq)]
716#[cfg_attr(
717    feature = "json",
718    derive(serde::Serialize, serde::Deserialize),
719    serde(rename_all = "camelCase", tag = "type")
720)]
721pub struct Emphasis {
722    pub values: Vec<Node>,
723    pub position: Option<Position>,
724}
725
726#[derive(Debug, Clone, PartialEq)]
727#[cfg_attr(
728    feature = "json",
729    derive(serde::Serialize, serde::Deserialize),
730    serde(rename_all = "camelCase", tag = "type")
731)]
732pub struct Strong {
733    pub values: Vec<Node>,
734    pub position: Option<Position>,
735}
736
737#[derive(Debug, Clone, PartialEq)]
738#[cfg_attr(
739    feature = "json",
740    derive(serde::Serialize, serde::Deserialize),
741    serde(rename_all = "camelCase", tag = "type")
742)]
743pub struct Break {
744    pub position: Option<Position>,
745}
746
747#[derive(Debug, Clone, PartialEq)]
748#[cfg_attr(
749    feature = "json",
750    derive(serde::Serialize, serde::Deserialize),
751    serde(rename_all = "camelCase", tag = "type")
752)]
753pub struct HorizontalRule {
754    pub position: Option<Position>,
755}
756
757#[derive(Debug, Clone, Default)]
758#[cfg_attr(
759    feature = "json",
760    derive(serde::Serialize, serde::Deserialize),
761    serde(rename_all = "camelCase", untagged)
762)]
763pub enum Node {
764    Blockquote(Blockquote),
765    Break(Break),
766    #[cfg(feature = "callout")]
767    Callout(Callout),
768    #[cfg(feature = "embed")]
769    Embed(Embed),
770    Definition(Definition),
771    Delete(Delete),
772    Heading(Heading),
773    Emphasis(Emphasis),
774    Footnote(Footnote),
775    FootnoteRef(FootnoteRef),
776    Html(Html),
777    Yaml(Yaml),
778    Toml(Toml),
779    Image(Image),
780    ImageRef(ImageRef),
781    CodeInline(CodeInline),
782    MathInline(MathInline),
783    Link(Link),
784    LinkRef(LinkRef),
785    #[cfg(feature = "wikilink")]
786    WikiLink(WikiLink),
787    Math(Math),
788    List(List),
789    TableAlign(TableAlign),
790    TableRow(TableRow),
791    TableCell(TableCell),
792    Code(Code),
793    Strong(Strong),
794    HorizontalRule(HorizontalRule),
795    MdxFlowExpression(MdxFlowExpression),
796    MdxJsxFlowElement(MdxJsxFlowElement),
797    MdxJsxTextElement(MdxJsxTextElement),
798    MdxTextExpression(MdxTextExpression),
799    MdxJsEsm(MdxJsEsm),
800    Text(Text),
801    Fragment(Fragment),
802    #[default]
803    Empty,
804}
805
806impl PartialEq for Node {
807    fn eq(&self, other: &Self) -> bool {
808        self.to_string() == other.to_string()
809    }
810}
811
812impl PartialOrd for Node {
813    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
814        let (self_node, other_node) = (self, other);
815        let self_pos = self_node.position();
816        let other_pos = other_node.position();
817
818        match (self_pos, other_pos) {
819            (Some(self_pos), Some(other_pos)) => match self_pos.start.line.cmp(&other_pos.start.line) {
820                std::cmp::Ordering::Equal => self_pos.start.column.partial_cmp(&other_pos.start.column),
821                ordering => Some(ordering),
822            },
823            (Some(_), None) => Some(std::cmp::Ordering::Less),
824            (None, Some(_)) => Some(std::cmp::Ordering::Greater),
825            (None, None) => Some(self.name().cmp(&other.name())),
826        }
827    }
828}
829
830#[derive(Debug, Clone, PartialEq)]
831#[cfg_attr(
832    feature = "json",
833    derive(serde::Serialize, serde::Deserialize),
834    serde(rename_all = "camelCase")
835)]
836pub struct Position {
837    pub start: Point,
838    pub end: Point,
839}
840
841#[derive(Debug, Clone, PartialEq)]
842#[cfg_attr(
843    feature = "json",
844    derive(serde::Serialize, serde::Deserialize),
845    serde(rename_all = "camelCase")
846)]
847pub struct Point {
848    pub line: usize,
849    pub column: usize,
850}
851
852impl From<markdown::unist::Position> for Position {
853    fn from(value: markdown::unist::Position) -> Self {
854        Self {
855            start: Point {
856                line: value.start.line,
857                column: value.start.column,
858            },
859            end: Point {
860                line: value.end.line,
861                column: value.end.column,
862            },
863        }
864    }
865}
866
867impl From<String> for Node {
868    fn from(value: String) -> Self {
869        Self::Text(Text { value, position: None })
870    }
871}
872
873impl From<&str> for Node {
874    fn from(value: &str) -> Self {
875        Self::Text(Text {
876            value: value.to_string(),
877            position: None,
878        })
879    }
880}
881
882impl Display for Node {
883    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
884        write!(f, "{}", self.to_string_with(&RenderOptions::default()))
885    }
886}
887
888impl Node {
889    pub fn map_values<E, F>(&self, f: &mut F) -> Result<Node, E>
890    where
891        E: std::error::Error,
892        F: FnMut(&Node) -> Result<Node, E>,
893    {
894        Self::_map_values(self.clone(), f)
895    }
896
897    fn _map_values<E, F>(node: Node, f: &mut F) -> Result<Node, E>
898    where
899        E: std::error::Error,
900        F: FnMut(&Node) -> Result<Node, E>,
901    {
902        match f(&node)? {
903            Node::Fragment(mut v) => {
904                let values = v
905                    .values
906                    .into_iter()
907                    .map(|node| Self::_map_values(node, f))
908                    .collect::<Result<Vec<_>, _>>();
909                match values {
910                    Ok(values) => {
911                        v.values = values;
912                        Ok(Node::Fragment(v))
913                    }
914                    Err(e) => Err(e),
915                }
916            }
917            node => Ok(node),
918        }
919    }
920
921    pub fn to_fragment(&self) -> Node {
922        match self.clone() {
923            Node::List(List { values, .. })
924            | Node::TableCell(TableCell { values, .. })
925            | Node::TableRow(TableRow { values, .. })
926            | Node::Link(Link { values, .. })
927            | Node::Footnote(Footnote { values, .. })
928            | Node::LinkRef(LinkRef { values, .. })
929            | Node::Heading(Heading { values, .. })
930            | Node::Blockquote(Blockquote { values, .. })
931            | Node::Delete(Delete { values, .. })
932            | Node::Emphasis(Emphasis { values, .. })
933            | Node::Strong(Strong { values, .. }) => Self::Fragment(Fragment { values }),
934            #[cfg(feature = "callout")]
935            Node::Callout(Callout { values, .. }) => Self::Fragment(Fragment { values }),
936            node @ Node::Fragment(_) => node,
937            _ => Self::Empty,
938        }
939    }
940
941    pub fn apply_fragment(&mut self, fragment: Node) {
942        Self::_apply_fragment(self, fragment)
943    }
944
945    fn _apply_fragment(node: &mut Node, fragment: Node) {
946        match node {
947            Node::List(List { values, .. })
948            | Node::TableCell(TableCell { values, .. })
949            | Node::TableRow(TableRow { values, .. })
950            | Node::Link(Link { values, .. })
951            | Node::Footnote(Footnote { values, .. })
952            | Node::LinkRef(LinkRef { values, .. })
953            | Node::Heading(Heading { values, .. })
954            | Node::Blockquote(Blockquote { values, .. })
955            | Node::Delete(Delete { values, .. })
956            | Node::Emphasis(Emphasis { values, .. })
957            | Node::Strong(Strong { values, .. }) => {
958                if let Node::Fragment(Fragment { values: new_values }) = fragment {
959                    let new_values = values
960                        .iter()
961                        .zip(new_values)
962                        .map(|(current_value, new_value)| {
963                            if new_value.is_empty() {
964                                current_value.clone()
965                            } else if new_value.is_fragment() {
966                                let mut current_value = current_value.clone();
967                                Self::_apply_fragment(&mut current_value, new_value);
968                                current_value
969                            } else {
970                                new_value
971                            }
972                        })
973                        .collect::<Vec<_>>();
974                    *values = new_values;
975                }
976            }
977            #[cfg(feature = "callout")]
978            Node::Callout(Callout { values, .. }) => {
979                if let Node::Fragment(Fragment { values: new_values }) = fragment {
980                    let new_values = values
981                        .iter()
982                        .zip(new_values)
983                        .map(|(current_value, new_value)| {
984                            if new_value.is_empty() {
985                                current_value.clone()
986                            } else if new_value.is_fragment() {
987                                let mut current_value = current_value.clone();
988                                Self::_apply_fragment(&mut current_value, new_value);
989                                current_value
990                            } else {
991                                new_value
992                            }
993                        })
994                        .collect::<Vec<_>>();
995                    *values = new_values;
996                }
997            }
998            _ => {}
999        }
1000    }
1001
1002    pub fn to_string_with(&self, options: &RenderOptions) -> String {
1003        self.render_with_theme(options, &ColorTheme::PLAIN)
1004    }
1005
1006    /// Returns a colored string representation of this node using ANSI escape codes.
1007    #[cfg(feature = "color")]
1008    pub fn to_colored_string_with(&self, options: &RenderOptions) -> String {
1009        self.render_with_theme(options, &ColorTheme::COLORED)
1010    }
1011
1012    pub(crate) fn render_with_theme(&self, options: &RenderOptions, theme: &ColorTheme<'_>) -> String {
1013        match self.clone() {
1014            Self::List(List {
1015                level,
1016                checked,
1017                values,
1018                ordered,
1019                index,
1020                ..
1021            }) => {
1022                let marker = if ordered {
1023                    format!("{}.", index + 1)
1024                } else {
1025                    options.list_style.to_string()
1026                };
1027                let (ms, me) = &theme.list_marker;
1028                format!(
1029                    "{}{}{}{} {}{}",
1030                    "  ".repeat(level as usize),
1031                    ms,
1032                    marker,
1033                    me,
1034                    checked.map(|it| if it { "[x] " } else { "[ ] " }).unwrap_or_else(|| ""),
1035                    render_values(&values, options, theme)
1036                )
1037            }
1038            Self::TableRow(TableRow { values, .. }) => {
1039                let (ts, te) = &theme.table_separator;
1040                let cells = values
1041                    .iter()
1042                    .map(|cell| cell.render_with_theme(options, theme))
1043                    .collect::<Vec<_>>()
1044                    .join("|");
1045                format!("{}|{}{}|", ts, te, cells)
1046            }
1047            Self::TableCell(TableCell { values, .. }) => render_values(&values, options, theme),
1048            Self::TableAlign(TableAlign { align, .. }) => {
1049                let (ts, te) = &theme.table_separator;
1050                format!("{}|{}|{}", ts, align.iter().map(|a| a.to_string()).join("|"), te)
1051            }
1052            Self::Blockquote(Blockquote { values, .. }) => {
1053                let (bs, be) = &theme.blockquote_marker;
1054                render_values_block(&values, options, theme)
1055                    .split('\n')
1056                    .map(|line| format!("{}> {}{}", bs, be, line))
1057                    .join("\n")
1058            }
1059            #[cfg(feature = "callout")]
1060            Self::Callout(Callout {
1061                kind, title, values, ..
1062            }) => {
1063                let (bs, be) = &theme.blockquote_marker;
1064                let header = match title.as_deref() {
1065                    Some(t) if !t.is_empty() => format!("[!{}] {}", kind, t),
1066                    _ => format!("[!{}]", kind),
1067                };
1068                let header_line = format!("{}> {}{}", bs, be, header);
1069                if values.is_empty() {
1070                    return header_line;
1071                }
1072                let body = render_values_block(&values, options, theme);
1073                if body.trim().is_empty() {
1074                    header_line
1075                } else {
1076                    let body_lines = body
1077                        .split('\n')
1078                        .map(|line| format!("{}> {}{}", bs, be, line))
1079                        .join("\n");
1080                    format!("{}\n{}", header_line, body_lines)
1081                }
1082            }
1083            #[cfg(feature = "embed")]
1084            Self::Embed(Embed { target, display, .. }) => match display.as_deref() {
1085                Some(d) if !d.is_empty() => format!("![[{}|{}]]", target, d),
1086                _ => format!("![[{}]]", target),
1087            },
1088            Self::Code(Code {
1089                value,
1090                lang,
1091                fence,
1092                meta,
1093                ..
1094            }) => {
1095                let (cs, ce) = &theme.code;
1096                let meta = meta.as_deref().map(|meta| format!(" {}", meta)).unwrap_or_default();
1097
1098                match lang {
1099                    Some(lang) => format!("{}```{}{}\n{}\n```{}", cs, lang, meta, value, ce),
1100                    None if fence => {
1101                        format!("{}```{}\n{}\n```{}", cs, lang.as_deref().unwrap_or(""), value, ce)
1102                    }
1103                    None => value.lines().map(|line| format!("{}    {}{}", cs, line, ce)).join("\n"),
1104                }
1105            }
1106            Self::Definition(Definition {
1107                ident,
1108                label,
1109                url,
1110                title,
1111                ..
1112            }) => {
1113                let (us, ue) = &theme.link_url;
1114                format!(
1115                    "[{}]: {}{}{}{}",
1116                    label.unwrap_or(ident),
1117                    us,
1118                    url.to_string_with(options),
1119                    ue,
1120                    title
1121                        .map(|title| format!(" {}", title.to_string_with(options)))
1122                        .unwrap_or_default()
1123                )
1124            }
1125            Self::Delete(Delete { values, .. }) => {
1126                let (ds, de) = &theme.delete;
1127                format!("{}~~{}~~{}", ds, render_values(&values, options, theme), de)
1128            }
1129            Self::Emphasis(Emphasis { values, .. }) => {
1130                let (es, ee) = &theme.emphasis;
1131                format!("{}*{}*{}", es, render_values(&values, options, theme), ee)
1132            }
1133            Self::Footnote(Footnote { values, ident, .. }) => {
1134                format!("[^{}]: {}", ident, render_values(&values, options, theme))
1135            }
1136            Self::FootnoteRef(FootnoteRef { label, .. }) => {
1137                format!("[^{}]", label.unwrap_or_default())
1138            }
1139            Self::Heading(Heading { depth, values, .. }) => {
1140                let (hs, he) = &theme.heading;
1141                format!(
1142                    "{}{} {}{}",
1143                    hs,
1144                    "#".repeat(depth as usize),
1145                    render_values(&values, options, theme),
1146                    he
1147                )
1148            }
1149            Self::Html(Html { value, .. }) => {
1150                let (hs, he) = &theme.html;
1151                format!("{}{}{}", hs, value, he)
1152            }
1153            Self::Image(Image { alt, url, title, .. }) => {
1154                let (is, ie) = &theme.image;
1155                format!(
1156                    "{}![{}]({}{}){}",
1157                    is,
1158                    alt,
1159                    url.replace(' ', "%20"),
1160                    title.map(|it| format!(" \"{}\"", it)).unwrap_or_default(),
1161                    ie
1162                )
1163            }
1164            Self::ImageRef(ImageRef { alt, ident, label, .. }) => {
1165                let (is, ie) = &theme.image;
1166                if alt == ident {
1167                    format!("{}![{}]{}", is, ident, ie)
1168                } else {
1169                    format!("{}![{}][{}]{}", is, alt, label.unwrap_or(ident), ie)
1170                }
1171            }
1172            Self::CodeInline(CodeInline { value, .. }) => {
1173                let (cs, ce) = &theme.code_inline;
1174                format!("{}`{}`{}", cs, value, ce)
1175            }
1176            Self::MathInline(MathInline { value, .. }) => {
1177                let (ms, me) = &theme.math;
1178                format!("{}${}${}", ms, value, me)
1179            }
1180            Self::Link(Link { url, title, values, .. }) => {
1181                let (ls, le) = &theme.link;
1182                format!(
1183                    "{}[{}]({}{}){}",
1184                    ls,
1185                    render_values(&values, options, theme),
1186                    url.to_string_with(options),
1187                    title
1188                        .map(|title| format!(" {}", title.to_string_with(options)))
1189                        .unwrap_or_default(),
1190                    le
1191                )
1192            }
1193            #[cfg(feature = "wikilink")]
1194            Self::WikiLink(WikiLink { target, text, .. }) => {
1195                let (ls, le) = &theme.link;
1196                match text.as_deref() {
1197                    Some(t) if t != target.as_str() => format!("{}[[{}|{}]]{}", ls, target, t, le),
1198                    _ => format!("{}[[{}]]{}", ls, target, le),
1199                }
1200            }
1201            Self::LinkRef(LinkRef { values, label, .. }) => {
1202                let (ls, le) = &theme.link;
1203                let ident = render_values(&values, options, theme);
1204
1205                label
1206                    .map(|label| {
1207                        if label == ident {
1208                            format!("{}[{}]{}", ls, ident, le)
1209                        } else {
1210                            format!("{}[{}][{}]{}", ls, ident, label, le)
1211                        }
1212                    })
1213                    .unwrap_or(format!("{}[{}]{}", ls, ident, le))
1214            }
1215            Self::Math(Math { value, .. }) => {
1216                let (ms, me) = &theme.math;
1217                format!("{}$$\n{}\n$${}", ms, value, me)
1218            }
1219            Self::Text(Text { value, .. }) => value,
1220            Self::MdxFlowExpression(mdx_flow_expression) => {
1221                format!("{{{}}}", mdx_flow_expression.value)
1222            }
1223            Self::MdxJsxFlowElement(mdx_jsx_flow_element) => {
1224                let name = mdx_jsx_flow_element.name.unwrap_or_default();
1225                let attributes = if mdx_jsx_flow_element.attributes.is_empty() {
1226                    "".to_string()
1227                } else {
1228                    format!(
1229                        " {}",
1230                        mdx_jsx_flow_element
1231                            .attributes
1232                            .into_iter()
1233                            .map(Self::mdx_attribute_content_to_string)
1234                            .join(" ")
1235                    )
1236                };
1237
1238                if mdx_jsx_flow_element.children.is_empty() {
1239                    format!("<{}{} />", name, attributes,)
1240                } else {
1241                    format!(
1242                        "<{}{}>{}</{}>",
1243                        name,
1244                        attributes,
1245                        render_values(&mdx_jsx_flow_element.children, options, theme),
1246                        name
1247                    )
1248                }
1249            }
1250            Self::MdxJsxTextElement(mdx_jsx_text_element) => {
1251                let name = mdx_jsx_text_element.name.unwrap_or_default();
1252                let attributes = if mdx_jsx_text_element.attributes.is_empty() {
1253                    "".to_string()
1254                } else {
1255                    format!(
1256                        " {}",
1257                        mdx_jsx_text_element
1258                            .attributes
1259                            .into_iter()
1260                            .map(Self::mdx_attribute_content_to_string)
1261                            .join(" ")
1262                    )
1263                };
1264
1265                if mdx_jsx_text_element.children.is_empty() {
1266                    format!("<{}{} />", name, attributes,)
1267                } else {
1268                    format!(
1269                        "<{}{}>{}</{}>",
1270                        name,
1271                        attributes,
1272                        render_values(&mdx_jsx_text_element.children, options, theme),
1273                        name
1274                    )
1275                }
1276            }
1277            Self::MdxTextExpression(mdx_text_expression) => {
1278                format!("{{{}}}", mdx_text_expression.value)
1279            }
1280            Self::MdxJsEsm(mdxjs_esm) => mdxjs_esm.value.to_string(),
1281            Self::Strong(Strong { values, .. }) => {
1282                let (ss, se) = &theme.strong;
1283                format!(
1284                    "{}**{}**{}",
1285                    ss,
1286                    values
1287                        .iter()
1288                        .map(|value| value.render_with_theme(options, theme))
1289                        .collect::<String>(),
1290                    se
1291                )
1292            }
1293            Self::Yaml(Yaml { value, .. }) => {
1294                let (fs, fe) = &theme.frontmatter;
1295                format!("{}---\n{}\n---{}", fs, value, fe)
1296            }
1297            Self::Toml(Toml { value, .. }) => {
1298                let (fs, fe) = &theme.frontmatter;
1299                format!("{}+++\n{}\n+++{}", fs, value, fe)
1300            }
1301            Self::Break(_) => "\\".to_string(),
1302            Self::HorizontalRule(_) => {
1303                let (hs, he) = &theme.horizontal_rule;
1304                format!("{}---{}", hs, he)
1305            }
1306            Self::Fragment(Fragment { values }) => values
1307                .iter()
1308                .map(|value| value.render_with_theme(options, theme))
1309                .filter(|s| !s.is_empty())
1310                .join("\n"),
1311            Self::Empty => String::new(),
1312        }
1313    }
1314
1315    pub fn node_values(&self) -> Vec<Node> {
1316        match self.clone() {
1317            Self::Blockquote(v) => v.values,
1318            Self::Delete(v) => v.values,
1319            Self::Heading(h) => h.values,
1320            Self::Emphasis(v) => v.values,
1321            Self::List(l) => l.values,
1322            Self::Strong(v) => v.values,
1323            #[cfg(feature = "callout")]
1324            Self::Callout(v) => v.values,
1325            _ => vec![self.clone()],
1326        }
1327    }
1328
1329    pub fn find_at_index(&self, index: usize) -> Option<Node> {
1330        match self {
1331            Self::Blockquote(v) => v.values.get(index).cloned(),
1332            Self::Delete(v) => v.values.get(index).cloned(),
1333            Self::Emphasis(v) => v.values.get(index).cloned(),
1334            Self::Strong(v) => v.values.get(index).cloned(),
1335            Self::Heading(v) => v.values.get(index).cloned(),
1336            Self::List(v) => v.values.get(index).cloned(),
1337            Self::TableCell(v) => v.values.get(index).cloned(),
1338            Self::TableRow(v) => v.values.get(index).cloned(),
1339            #[cfg(feature = "callout")]
1340            Self::Callout(v) => v.values.get(index).cloned(),
1341            _ => None,
1342        }
1343    }
1344
1345    pub fn value(&self) -> String {
1346        match self.clone() {
1347            Self::Blockquote(v) => values_to_value(v.values),
1348            Self::Definition(d) => d.url.as_str().to_string(),
1349            Self::Delete(v) => values_to_value(v.values),
1350            Self::Heading(h) => values_to_value(h.values),
1351            Self::Emphasis(v) => values_to_value(v.values),
1352            Self::Footnote(f) => values_to_value(f.values),
1353            Self::FootnoteRef(f) => f.ident,
1354            Self::Html(v) => v.value,
1355            Self::Yaml(v) => v.value,
1356            Self::Toml(v) => v.value,
1357            Self::Image(i) => i.url,
1358            Self::ImageRef(i) => i.ident,
1359            Self::CodeInline(v) => v.value.to_string(),
1360            Self::MathInline(v) => v.value.to_string(),
1361            Self::Link(l) => l.url.as_str().to_string(),
1362            Self::LinkRef(l) => l.ident,
1363            #[cfg(feature = "wikilink")]
1364            Self::WikiLink(w) => w.text.unwrap_or(w.target),
1365            #[cfg(feature = "callout")]
1366            Self::Callout(v) => values_to_value(v.values),
1367            #[cfg(feature = "embed")]
1368            Self::Embed(e) => e.display.unwrap_or(e.target),
1369            Self::Math(v) => v.value,
1370            Self::List(l) => values_to_value(l.values),
1371            Self::TableCell(c) => values_to_value(c.values),
1372            Self::TableRow(c) => values_to_value(c.values),
1373            Self::Code(c) => c.value,
1374            Self::Strong(v) => values_to_value(v.values),
1375            Self::Text(t) => t.value,
1376            Self::Break { .. } => String::new(),
1377            Self::TableAlign(_) => String::new(),
1378            Self::MdxFlowExpression(mdx) => mdx.value.to_string(),
1379            Self::MdxJsxFlowElement(mdx) => values_to_value(mdx.children),
1380            Self::MdxTextExpression(mdx) => mdx.value.to_string(),
1381            Self::MdxJsxTextElement(mdx) => values_to_value(mdx.children),
1382            Self::MdxJsEsm(mdx) => mdx.value.to_string(),
1383            Self::HorizontalRule { .. } => String::new(),
1384            Self::Fragment(v) => values_to_value(v.values),
1385            Self::Empty => String::new(),
1386        }
1387    }
1388
1389    pub fn name(&self) -> SmolStr {
1390        match self {
1391            Self::Blockquote(_) => "blockquote".into(),
1392            Self::Break { .. } => "break".into(),
1393            Self::Definition(_) => "definition".into(),
1394            Self::Delete(_) => "delete".into(),
1395            Self::Heading(Heading { depth, .. }) => match depth {
1396                1 => "h1".into(),
1397                2 => "h2".into(),
1398                3 => "h3".into(),
1399                4 => "h4".into(),
1400                5 => "h5".into(),
1401                6 => "h6".into(),
1402                _ => "h".into(),
1403            },
1404            Self::Emphasis(_) => "emphasis".into(),
1405            Self::Footnote(_) => "footnote".into(),
1406            Self::FootnoteRef(_) => "footnoteref".into(),
1407            Self::Html(_) => "html".into(),
1408            Self::Yaml(_) => "yaml".into(),
1409            Self::Toml(_) => "toml".into(),
1410            Self::Image(_) => "image".into(),
1411            Self::ImageRef(_) => "image_ref".into(),
1412            Self::CodeInline(_) => "code_inline".into(),
1413            Self::MathInline(_) => "math_inline".into(),
1414            Self::Link(_) => "link".into(),
1415            Self::LinkRef(_) => "link_ref".into(),
1416            #[cfg(feature = "wikilink")]
1417            Self::WikiLink(_) => "wikilink".into(),
1418            #[cfg(feature = "callout")]
1419            Self::Callout(_) => "callout".into(),
1420            #[cfg(feature = "embed")]
1421            Self::Embed(_) => "embed".into(),
1422            Self::Math(_) => "math".into(),
1423            Self::List(_) => "list".into(),
1424            Self::TableAlign(_) => "table_align".into(),
1425            Self::TableRow(_) => "table_row".into(),
1426            Self::TableCell(_) => "table_cell".into(),
1427            Self::Code(_) => "code".into(),
1428            Self::Strong(_) => "strong".into(),
1429            Self::HorizontalRule { .. } => "Horizontal_rule".into(),
1430            Self::MdxFlowExpression(_) => "mdx_flow_expression".into(),
1431            Self::MdxJsxFlowElement(_) => "mdx_jsx_flow_element".into(),
1432            Self::MdxJsxTextElement(_) => "mdx_jsx_text_element".into(),
1433            Self::MdxTextExpression(_) => "mdx_text_expression".into(),
1434            Self::MdxJsEsm(_) => "mdx_js_esm".into(),
1435            Self::Text(_) => "text".into(),
1436            Self::Fragment(_) | Self::Empty => "".into(),
1437        }
1438    }
1439
1440    /// Get the children nodes of the current node.
1441    pub fn children(&self) -> Vec<Node> {
1442        match self.attr(CHILDREN) {
1443            Some(AttrValue::Array(children)) => children,
1444            _ => Vec::new(),
1445        }
1446    }
1447
1448    pub fn set_position(&mut self, pos: Option<Position>) {
1449        match self {
1450            Self::Blockquote(v) => v.position = pos,
1451            Self::Definition(d) => d.position = pos,
1452            Self::Delete(v) => v.position = pos,
1453            Self::Heading(h) => h.position = pos,
1454            Self::Emphasis(v) => v.position = pos,
1455            Self::Footnote(f) => f.position = pos,
1456            Self::FootnoteRef(f) => f.position = pos,
1457            Self::Html(v) => v.position = pos,
1458            Self::Yaml(v) => v.position = pos,
1459            Self::Toml(v) => v.position = pos,
1460            Self::Image(i) => i.position = pos,
1461            Self::ImageRef(i) => i.position = pos,
1462            Self::CodeInline(v) => v.position = pos,
1463            Self::MathInline(v) => v.position = pos,
1464            Self::Link(l) => l.position = pos,
1465            Self::LinkRef(l) => l.position = pos,
1466            #[cfg(feature = "wikilink")]
1467            Self::WikiLink(w) => w.position = pos,
1468            #[cfg(feature = "callout")]
1469            Self::Callout(c) => c.position = pos,
1470            #[cfg(feature = "embed")]
1471            Self::Embed(e) => e.position = pos,
1472            Self::Math(v) => v.position = pos,
1473            Self::Code(c) => c.position = pos,
1474            Self::TableCell(c) => c.position = pos,
1475            Self::TableRow(r) => r.position = pos,
1476            Self::TableAlign(c) => c.position = pos,
1477            Self::List(l) => l.position = pos,
1478            Self::Strong(s) => s.position = pos,
1479            Self::MdxFlowExpression(m) => m.position = pos,
1480            Self::MdxTextExpression(m) => m.position = pos,
1481            Self::MdxJsEsm(m) => m.position = pos,
1482            Self::MdxJsxFlowElement(m) => m.position = pos,
1483            Self::MdxJsxTextElement(m) => m.position = pos,
1484            Self::Break(b) => b.position = pos,
1485            Self::HorizontalRule(h) => h.position = pos,
1486            Self::Text(t) => t.position = pos,
1487            Self::Fragment(_) | Self::Empty => {}
1488        }
1489    }
1490
1491    pub fn position(&self) -> Option<Position> {
1492        match self {
1493            Self::Blockquote(v) => v.position.clone(),
1494            Self::Definition(d) => d.position.clone(),
1495            Self::Delete(v) => v.position.clone(),
1496            Self::Heading(h) => h.position.clone(),
1497            Self::Emphasis(v) => v.position.clone(),
1498            Self::Footnote(f) => f.position.clone(),
1499            Self::FootnoteRef(f) => f.position.clone(),
1500            Self::Html(v) => v.position.clone(),
1501            Self::Yaml(v) => v.position.clone(),
1502            Self::Toml(v) => v.position.clone(),
1503            Self::Image(i) => i.position.clone(),
1504            Self::ImageRef(i) => i.position.clone(),
1505            Self::CodeInline(v) => v.position.clone(),
1506            Self::MathInline(v) => v.position.clone(),
1507            Self::Link(l) => l.position.clone(),
1508            Self::LinkRef(l) => l.position.clone(),
1509            #[cfg(feature = "wikilink")]
1510            Self::WikiLink(w) => w.position.clone(),
1511            #[cfg(feature = "callout")]
1512            Self::Callout(c) => c.position.clone(),
1513            #[cfg(feature = "embed")]
1514            Self::Embed(e) => e.position.clone(),
1515            Self::Math(v) => v.position.clone(),
1516            Self::Code(c) => c.position.clone(),
1517            Self::TableCell(c) => c.position.clone(),
1518            Self::TableRow(r) => r.position.clone(),
1519            Self::TableAlign(c) => c.position.clone(),
1520            Self::List(l) => l.position.clone(),
1521            Self::Strong(s) => s.position.clone(),
1522            Self::MdxFlowExpression(m) => m.position.clone(),
1523            Self::MdxTextExpression(m) => m.position.clone(),
1524            Self::MdxJsEsm(m) => m.position.clone(),
1525            Self::MdxJsxFlowElement(m) => m.position.clone(),
1526            Self::MdxJsxTextElement(m) => m.position.clone(),
1527            Self::Break(b) => b.position.clone(),
1528            Self::Text(t) => t.position.clone(),
1529            Self::HorizontalRule(h) => h.position.clone(),
1530            Self::Fragment(v) => {
1531                let positions: Vec<Position> = v.values.iter().filter_map(|node| node.position()).collect();
1532
1533                match (positions.first(), positions.last()) {
1534                    (Some(start), Some(end)) => Some(Position {
1535                        start: start.start.clone(),
1536                        end: end.end.clone(),
1537                    }),
1538                    _ => None,
1539                }
1540            }
1541            Self::Empty => None,
1542        }
1543    }
1544
1545    pub fn is_empty(&self) -> bool {
1546        matches!(self, Self::Empty)
1547    }
1548
1549    pub fn is_fragment(&self) -> bool {
1550        matches!(self, Self::Fragment(_))
1551    }
1552
1553    pub fn is_empty_fragment(&self) -> bool {
1554        if let Self::Fragment(_) = self {
1555            Self::_fragment_inner_nodes(self).is_empty()
1556        } else {
1557            false
1558        }
1559    }
1560
1561    fn _fragment_inner_nodes(node: &Node) -> Vec<Node> {
1562        if let Self::Fragment(fragment) = node {
1563            fragment.values.iter().flat_map(Self::_fragment_inner_nodes).collect()
1564        } else if node.is_empty() {
1565            Vec::new()
1566        } else {
1567            vec![node.clone()]
1568        }
1569    }
1570
1571    pub fn is_inline_code(&self) -> bool {
1572        matches!(self, Self::CodeInline(_))
1573    }
1574
1575    pub fn is_inline_math(&self) -> bool {
1576        matches!(self, Self::MathInline(_))
1577    }
1578
1579    pub fn is_strong(&self) -> bool {
1580        matches!(self, Self::Strong(_))
1581    }
1582
1583    pub fn is_list(&self) -> bool {
1584        matches!(self, Self::List(_))
1585    }
1586
1587    pub fn is_emphasis(&self) -> bool {
1588        matches!(self, Self::Emphasis(_))
1589    }
1590
1591    pub fn is_delete(&self) -> bool {
1592        matches!(self, Self::Delete(_))
1593    }
1594
1595    pub fn is_link(&self) -> bool {
1596        #[cfg(feature = "wikilink")]
1597        {
1598            matches!(self, Self::Link(_) | Self::WikiLink(_))
1599        }
1600        #[cfg(not(feature = "wikilink"))]
1601        {
1602            matches!(self, Self::Link(_))
1603        }
1604    }
1605
1606    pub fn is_link_ref(&self) -> bool {
1607        matches!(self, Self::LinkRef(_))
1608    }
1609
1610    /// Returns `true` if this node is an Obsidian-style wikilink (`[[target]]`).
1611    #[cfg(feature = "wikilink")]
1612    pub fn is_wikilink(&self) -> bool {
1613        matches!(self, Self::WikiLink(_))
1614    }
1615
1616    /// Returns `true` if this node is an Obsidian-style callout (`> [!TYPE]`).
1617    #[cfg(feature = "callout")]
1618    pub fn is_callout(&self) -> bool {
1619        matches!(self, Self::Callout(_))
1620    }
1621
1622    /// Returns `true` if this node is an Obsidian-style embed (`![[target]]`).
1623    #[cfg(feature = "embed")]
1624    pub fn is_embed(&self) -> bool {
1625        matches!(self, Self::Embed(_))
1626    }
1627
1628    pub fn is_text(&self) -> bool {
1629        matches!(self, Self::Text(_))
1630    }
1631
1632    pub fn is_image(&self) -> bool {
1633        matches!(self, Self::Image(_))
1634    }
1635
1636    pub fn is_horizontal_rule(&self) -> bool {
1637        matches!(self, Self::HorizontalRule { .. })
1638    }
1639
1640    pub fn is_blockquote(&self) -> bool {
1641        matches!(self, Self::Blockquote(_))
1642    }
1643
1644    pub fn is_html(&self) -> bool {
1645        matches!(self, Self::Html { .. })
1646    }
1647
1648    pub fn is_footnote(&self) -> bool {
1649        matches!(self, Self::Footnote(_))
1650    }
1651
1652    pub fn is_mdx_jsx_flow_element(&self) -> bool {
1653        matches!(self, Self::MdxJsxFlowElement(MdxJsxFlowElement { .. }))
1654    }
1655
1656    pub fn is_mdx_js_esm(&self) -> bool {
1657        matches!(self, Self::MdxJsEsm(MdxJsEsm { .. }))
1658    }
1659
1660    pub fn is_toml(&self) -> bool {
1661        matches!(self, Self::Toml { .. })
1662    }
1663
1664    pub fn is_yaml(&self) -> bool {
1665        matches!(self, Self::Yaml { .. })
1666    }
1667
1668    pub fn is_break(&self) -> bool {
1669        matches!(self, Self::Break { .. })
1670    }
1671
1672    pub fn is_mdx_text_expression(&self) -> bool {
1673        matches!(self, Self::MdxTextExpression(MdxTextExpression { .. }))
1674    }
1675
1676    pub fn is_footnote_ref(&self) -> bool {
1677        matches!(self, Self::FootnoteRef { .. })
1678    }
1679
1680    pub fn is_image_ref(&self) -> bool {
1681        matches!(self, Self::ImageRef(_))
1682    }
1683
1684    pub fn is_mdx_jsx_text_element(&self) -> bool {
1685        matches!(self, Self::MdxJsxTextElement(MdxJsxTextElement { .. }))
1686    }
1687
1688    pub fn is_math(&self) -> bool {
1689        matches!(self, Self::Math(_))
1690    }
1691
1692    pub fn is_mdx_flow_expression(&self) -> bool {
1693        matches!(self, Self::MdxFlowExpression(MdxFlowExpression { .. }))
1694    }
1695
1696    pub fn is_definition(&self) -> bool {
1697        matches!(self, Self::Definition(_))
1698    }
1699
1700    pub fn is_table_align(&self) -> bool {
1701        matches!(self, Self::TableAlign(_))
1702    }
1703
1704    pub fn is_code(&self, lang: Option<SmolStr>) -> bool {
1705        if let Self::Code(Code { lang: node_lang, .. }) = &self {
1706            if lang.is_none() {
1707                true
1708            } else {
1709                node_lang.clone().unwrap_or_default() == lang.unwrap_or_default()
1710            }
1711        } else {
1712            false
1713        }
1714    }
1715
1716    pub fn is_heading(&self, depth: Option<u8>) -> bool {
1717        if let Self::Heading(Heading {
1718            depth: heading_depth, ..
1719        }) = &self
1720        {
1721            depth.is_none() || *heading_depth == depth.unwrap()
1722        } else {
1723            false
1724        }
1725    }
1726
1727    pub fn with_value(&self, value: &str) -> Self {
1728        match self.clone() {
1729            Self::Blockquote(mut v) => {
1730                if let Some(node) = v.values.first() {
1731                    v.values[0] = node.with_value(value);
1732                }
1733
1734                Self::Blockquote(v)
1735            }
1736            Self::Delete(mut v) => {
1737                if let Some(node) = v.values.first() {
1738                    v.values[0] = node.with_value(value);
1739                }
1740
1741                Self::Delete(v)
1742            }
1743            Self::Emphasis(mut v) => {
1744                if let Some(node) = v.values.first() {
1745                    v.values[0] = node.with_value(value);
1746                }
1747
1748                Self::Emphasis(v)
1749            }
1750            Self::Html(mut html) => {
1751                html.value = value.to_string();
1752                Self::Html(html)
1753            }
1754            Self::Yaml(mut yaml) => {
1755                yaml.value = value.to_string();
1756                Self::Yaml(yaml)
1757            }
1758            Self::Toml(mut toml) => {
1759                toml.value = value.to_string();
1760                Self::Toml(toml)
1761            }
1762            Self::CodeInline(mut code) => {
1763                code.value = value.into();
1764                Self::CodeInline(code)
1765            }
1766            Self::MathInline(mut math) => {
1767                math.value = value.into();
1768                Self::MathInline(math)
1769            }
1770            Self::Math(mut math) => {
1771                math.value = value.to_string();
1772                Self::Math(math)
1773            }
1774            Self::List(mut v) => {
1775                if let Some(node) = v.values.first() {
1776                    v.values[0] = node.with_value(value);
1777                }
1778
1779                Self::List(v)
1780            }
1781            Self::TableCell(mut v) => {
1782                if let Some(node) = v.values.first() {
1783                    v.values[0] = node.with_value(value);
1784                }
1785
1786                Self::TableCell(v)
1787            }
1788            Self::TableRow(mut row) => {
1789                row.values = row
1790                    .values
1791                    .iter()
1792                    .zip(value.split(","))
1793                    .map(|(cell, value)| cell.with_value(value))
1794                    .collect::<Vec<_>>();
1795
1796                Self::TableRow(row)
1797            }
1798            Self::Strong(mut v) => {
1799                if let Some(node) = v.values.first() {
1800                    v.values[0] = node.with_value(value);
1801                }
1802
1803                Self::Strong(v)
1804            }
1805            Self::Code(mut code) => {
1806                code.value = value.to_string();
1807                Self::Code(code)
1808            }
1809            Self::Image(mut image) => {
1810                image.url = value.to_string();
1811                Self::Image(image)
1812            }
1813            Self::ImageRef(mut image) => {
1814                image.ident = value.to_string();
1815                image.label = Some(value.to_string());
1816                Self::ImageRef(image)
1817            }
1818            Self::Link(mut link) => {
1819                link.url = Url(value.to_string());
1820                Self::Link(link)
1821            }
1822            Self::LinkRef(mut v) => {
1823                v.label = Some(value.to_string());
1824                v.ident = value.to_string();
1825                Self::LinkRef(v)
1826            }
1827            Self::Footnote(mut footnote) => {
1828                footnote.ident = value.to_string();
1829                Self::Footnote(footnote)
1830            }
1831            Self::FootnoteRef(mut footnote) => {
1832                footnote.ident = value.to_string();
1833                footnote.label = Some(value.to_string());
1834                Self::FootnoteRef(footnote)
1835            }
1836            Self::Heading(mut v) => {
1837                if let Some(node) = v.values.first() {
1838                    v.values[0] = node.with_value(value);
1839                }
1840
1841                Self::Heading(v)
1842            }
1843            Self::Definition(mut def) => {
1844                def.url = Url(value.to_string());
1845                Self::Definition(def)
1846            }
1847            node @ Self::Break { .. } => node,
1848            node @ Self::TableAlign(_) => node,
1849            node @ Self::HorizontalRule { .. } => node,
1850            Self::Text(mut text) => {
1851                text.value = value.to_string();
1852                Self::Text(text)
1853            }
1854            Self::MdxFlowExpression(mut mdx) => {
1855                mdx.value = value.into();
1856                Self::MdxFlowExpression(mdx)
1857            }
1858            Self::MdxTextExpression(mut mdx) => {
1859                mdx.value = value.into();
1860                Self::MdxTextExpression(mdx)
1861            }
1862            Self::MdxJsEsm(mut mdx) => {
1863                mdx.value = value.into();
1864                Self::MdxJsEsm(mdx)
1865            }
1866            Self::MdxJsxFlowElement(mut mdx) => {
1867                if let Some(node) = mdx.children.first() {
1868                    mdx.children[0] = node.with_value(value);
1869                }
1870
1871                Self::MdxJsxFlowElement(MdxJsxFlowElement {
1872                    name: mdx.name,
1873                    attributes: mdx.attributes,
1874                    children: mdx.children,
1875                    ..mdx
1876                })
1877            }
1878            Self::MdxJsxTextElement(mut mdx) => {
1879                if let Some(node) = mdx.children.first() {
1880                    mdx.children[0] = node.with_value(value);
1881                }
1882
1883                Self::MdxJsxTextElement(MdxJsxTextElement {
1884                    name: mdx.name,
1885                    attributes: mdx.attributes,
1886                    children: mdx.children,
1887                    ..mdx
1888                })
1889            }
1890            node @ Self::Fragment(_) | node @ Self::Empty => node,
1891            #[cfg(feature = "wikilink")]
1892            Self::WikiLink(mut w) => {
1893                if w.text.is_some() {
1894                    w.text = Some(value.to_string());
1895                } else {
1896                    w.target = value.to_string();
1897                }
1898                Self::WikiLink(w)
1899            }
1900            #[cfg(feature = "callout")]
1901            Self::Callout(mut c) => {
1902                if let Some(node) = c.values.first() {
1903                    c.values[0] = node.with_value(value);
1904                }
1905                Self::Callout(c)
1906            }
1907            #[cfg(feature = "embed")]
1908            Self::Embed(mut e) => {
1909                if e.display.is_some() {
1910                    e.display = Some(value.to_string());
1911                } else {
1912                    e.target = value.to_string();
1913                }
1914                Self::Embed(e)
1915            }
1916        }
1917    }
1918
1919    pub fn with_children_value(&self, value: &str, index: usize) -> Self {
1920        match self.clone() {
1921            Self::Blockquote(mut v) => {
1922                if v.values.get(index).is_some() {
1923                    v.values[index] = v.values[index].with_value(value);
1924                }
1925
1926                Self::Blockquote(v)
1927            }
1928            Self::Delete(mut v) => {
1929                if v.values.get(index).is_some() {
1930                    v.values[index] = v.values[index].with_value(value);
1931                }
1932
1933                Self::Delete(v)
1934            }
1935            Self::Emphasis(mut v) => {
1936                if v.values.get(index).is_some() {
1937                    v.values[index] = v.values[index].with_value(value);
1938                }
1939
1940                Self::Emphasis(v)
1941            }
1942            Self::List(mut v) => {
1943                if v.values.get(index).is_some() {
1944                    v.values[index] = v.values[index].with_value(value);
1945                }
1946
1947                Self::List(v)
1948            }
1949            Self::TableCell(mut v) => {
1950                if v.values.get(index).is_some() {
1951                    v.values[index] = v.values[index].with_value(value);
1952                }
1953
1954                Self::TableCell(v)
1955            }
1956            Self::Strong(mut v) => {
1957                if v.values.get(index).is_some() {
1958                    v.values[index] = v.values[index].with_value(value);
1959                }
1960
1961                Self::Strong(v)
1962            }
1963            Self::LinkRef(mut v) => {
1964                if v.values.get(index).is_some() {
1965                    v.values[index] = v.values[index].with_value(value);
1966                }
1967
1968                Self::LinkRef(v)
1969            }
1970            Self::Heading(mut v) => {
1971                if v.values.get(index).is_some() {
1972                    v.values[index] = v.values[index].with_value(value);
1973                }
1974
1975                Self::Heading(v)
1976            }
1977            Self::MdxJsxFlowElement(mut mdx) => {
1978                if let Some(node) = mdx.children.first() {
1979                    mdx.children[index] = node.with_value(value);
1980                }
1981
1982                Self::MdxJsxFlowElement(MdxJsxFlowElement {
1983                    name: mdx.name,
1984                    attributes: mdx.attributes,
1985                    children: mdx.children,
1986                    ..mdx
1987                })
1988            }
1989            Self::MdxJsxTextElement(mut mdx) => {
1990                if let Some(node) = mdx.children.first() {
1991                    mdx.children[index] = node.with_value(value);
1992                }
1993
1994                Self::MdxJsxTextElement(MdxJsxTextElement {
1995                    name: mdx.name,
1996                    attributes: mdx.attributes,
1997                    children: mdx.children,
1998                    ..mdx
1999                })
2000            }
2001            #[cfg(feature = "wikilink")]
2002            a @ Self::WikiLink(_) => a,
2003            #[cfg(feature = "callout")]
2004            Self::Callout(mut c) => {
2005                if c.values.get(index).is_some() {
2006                    c.values[index] = c.values[index].with_value(value);
2007                }
2008                Self::Callout(c)
2009            }
2010            #[cfg(feature = "embed")]
2011            a @ Self::Embed(_) => a,
2012            a => a,
2013        }
2014    }
2015
2016    /// Returns the value of the specified attribute, if present.
2017    pub fn attr(&self, attr: &str) -> Option<AttrValue> {
2018        match self {
2019            Node::Footnote(Footnote { ident, values, .. }) => match attr {
2020                attr_keys::IDENT => Some(AttrValue::String(ident.clone())),
2021                attr_keys::VALUE => Some(AttrValue::String(values_to_string(values, &RenderOptions::default()))),
2022                attr_keys::VALUES | attr_keys::CHILDREN => Some(AttrValue::Array(values.clone())),
2023                _ => None,
2024            },
2025            Node::Html(Html { value, .. }) => match attr {
2026                attr_keys::VALUE => Some(AttrValue::String(value.clone())),
2027                _ => None,
2028            },
2029            Node::Text(Text { value, .. }) => match attr {
2030                attr_keys::VALUE => Some(AttrValue::String(value.clone())),
2031                _ => None,
2032            },
2033            Node::Code(Code {
2034                value,
2035                lang,
2036                meta,
2037                fence,
2038                ..
2039            }) => match attr {
2040                attr_keys::VALUE => Some(AttrValue::String(value.clone())),
2041                attr_keys::LANG => lang.clone().map(AttrValue::String),
2042                attr_keys::META => meta.clone().map(AttrValue::String),
2043                attr_keys::FENCE => Some(AttrValue::Boolean(*fence)),
2044                _ => None,
2045            },
2046            Node::CodeInline(CodeInline { value, .. }) => match attr {
2047                attr_keys::VALUE => Some(AttrValue::String(value.to_string())),
2048                _ => None,
2049            },
2050            Node::MathInline(MathInline { value, .. }) => match attr {
2051                attr_keys::VALUE => Some(AttrValue::String(value.to_string())),
2052                _ => None,
2053            },
2054            Node::Math(Math { value, .. }) => match attr {
2055                attr_keys::VALUE => Some(AttrValue::String(value.clone())),
2056                _ => None,
2057            },
2058            Node::Yaml(Yaml { value, .. }) => match attr {
2059                attr_keys::VALUE => Some(AttrValue::String(value.clone())),
2060                _ => None,
2061            },
2062            Node::Toml(Toml { value, .. }) => match attr {
2063                attr_keys::VALUE => Some(AttrValue::String(value.clone())),
2064                _ => None,
2065            },
2066            Node::Image(Image { alt, url, title, .. }) => match attr {
2067                attr_keys::ALT => Some(AttrValue::String(alt.clone())),
2068                attr_keys::URL => Some(AttrValue::String(url.clone())),
2069                attr_keys::TITLE => title.clone().map(AttrValue::String),
2070                _ => None,
2071            },
2072            Node::ImageRef(ImageRef { alt, ident, label, .. }) => match attr {
2073                attr_keys::ALT => Some(AttrValue::String(alt.clone())),
2074                attr_keys::IDENT => Some(AttrValue::String(ident.clone())),
2075                attr_keys::LABEL => label.clone().map(AttrValue::String),
2076                _ => None,
2077            },
2078            Node::Link(Link { url, title, values, .. }) => match attr {
2079                attr_keys::URL => Some(AttrValue::String(url.as_str().to_string())),
2080                attr_keys::TITLE => title.as_ref().map(|t| AttrValue::String(t.to_value())),
2081                attr_keys::VALUE => Some(AttrValue::String(values_to_string(values, &RenderOptions::default()))),
2082                attr_keys::VALUES | attr_keys::CHILDREN => Some(AttrValue::Array(values.clone())),
2083                _ => None,
2084            },
2085            #[cfg(feature = "wikilink")]
2086            Node::WikiLink(WikiLink { target, text, .. }) => match attr {
2087                attr_keys::URL => Some(AttrValue::String(target.clone())),
2088                attr_keys::VALUE => Some(AttrValue::String(text.clone().unwrap_or_else(|| target.clone()))),
2089                _ => None,
2090            },
2091            #[cfg(feature = "callout")]
2092            Node::Callout(Callout {
2093                kind, title, values, ..
2094            }) => match attr {
2095                attr_keys::KIND => Some(AttrValue::String(kind.clone())),
2096                attr_keys::TITLE => title.clone().map(AttrValue::String),
2097                attr_keys::VALUE => Some(AttrValue::String(values_to_string(values, &RenderOptions::default()))),
2098                attr_keys::VALUES | attr_keys::CHILDREN => Some(AttrValue::Array(values.clone())),
2099                _ => None,
2100            },
2101            #[cfg(feature = "embed")]
2102            Node::Embed(Embed { target, display, .. }) => match attr {
2103                attr_keys::URL => Some(AttrValue::String(target.clone())),
2104                attr_keys::VALUE => Some(AttrValue::String(display.clone().unwrap_or_else(|| target.clone()))),
2105                _ => None,
2106            },
2107            Node::LinkRef(LinkRef { ident, label, .. }) => match attr {
2108                attr_keys::IDENT => Some(AttrValue::String(ident.clone())),
2109                attr_keys::LABEL => label.clone().map(AttrValue::String),
2110                _ => None,
2111            },
2112            Node::FootnoteRef(FootnoteRef { ident, label, .. }) => match attr {
2113                attr_keys::IDENT => Some(AttrValue::String(ident.clone())),
2114                attr_keys::LABEL => label.clone().map(AttrValue::String),
2115                _ => None,
2116            },
2117            Node::Definition(Definition {
2118                ident,
2119                url,
2120                title,
2121                label,
2122                ..
2123            }) => match attr {
2124                attr_keys::IDENT => Some(AttrValue::String(ident.clone())),
2125                attr_keys::URL => Some(AttrValue::String(url.as_str().to_string())),
2126                attr_keys::TITLE => title.as_ref().map(|t| AttrValue::String(t.to_value())),
2127                attr_keys::LABEL => label.clone().map(AttrValue::String),
2128                _ => None,
2129            },
2130            Node::Heading(Heading { depth, values, .. }) => match attr {
2131                attr_keys::DEPTH | attr_keys::LEVEL => Some(AttrValue::Integer(*depth as i64)),
2132                attr_keys::VALUE => Some(AttrValue::String(values_to_string(values, &RenderOptions::default()))),
2133                attr_keys::VALUES | attr_keys::CHILDREN => Some(AttrValue::Array(values.clone())),
2134                _ => None,
2135            },
2136            Node::List(List {
2137                index,
2138                level,
2139                ordered,
2140                checked,
2141                values,
2142                ..
2143            }) => match attr {
2144                attr_keys::INDEX => Some(AttrValue::Integer(*index as i64)),
2145                attr_keys::LEVEL => Some(AttrValue::Integer(*level as i64)),
2146                attr_keys::ORDERED => Some(AttrValue::Boolean(*ordered)),
2147                attr_keys::CHECKED => checked.map(AttrValue::Boolean),
2148                attr_keys::VALUE => Some(AttrValue::String(values_to_string(values, &RenderOptions::default()))),
2149                attr_keys::VALUES | attr_keys::CHILDREN => Some(AttrValue::Array(values.clone())),
2150                _ => None,
2151            },
2152            Node::TableCell(TableCell {
2153                column, row, values, ..
2154            }) => match attr {
2155                attr_keys::COLUMN => Some(AttrValue::Integer(*column as i64)),
2156                attr_keys::ROW => Some(AttrValue::Integer(*row as i64)),
2157                attr_keys::VALUE => Some(AttrValue::String(values_to_string(values, &RenderOptions::default()))),
2158                attr_keys::VALUES | attr_keys::CHILDREN => Some(AttrValue::Array(values.clone())),
2159                _ => None,
2160            },
2161            Node::TableAlign(TableAlign { align, .. }) => match attr {
2162                attr_keys::ALIGN => Some(AttrValue::String(
2163                    align.iter().map(|a| a.to_string()).collect::<Vec<_>>().join(","),
2164                )),
2165                _ => None,
2166            },
2167            Node::MdxFlowExpression(MdxFlowExpression { value, .. }) => match attr {
2168                attr_keys::VALUE => Some(AttrValue::String(value.to_string())),
2169                _ => None,
2170            },
2171            Node::MdxTextExpression(MdxTextExpression { value, .. }) => match attr {
2172                attr_keys::VALUE => Some(AttrValue::String(value.to_string())),
2173                _ => None,
2174            },
2175            Node::MdxJsEsm(MdxJsEsm { value, .. }) => match attr {
2176                attr_keys::VALUE => Some(AttrValue::String(value.to_string())),
2177                _ => None,
2178            },
2179            Node::MdxJsxFlowElement(MdxJsxFlowElement { name, .. }) => match attr {
2180                attr_keys::NAME => name.clone().map(AttrValue::String),
2181                _ => None,
2182            },
2183            Node::MdxJsxTextElement(MdxJsxTextElement { name, .. }) => match attr {
2184                attr_keys::NAME => name.as_ref().map(|n| AttrValue::String(n.to_string())),
2185                _ => None,
2186            },
2187            Node::Strong(Strong { values, .. })
2188            | Node::Blockquote(Blockquote { values, .. })
2189            | Node::Delete(Delete { values, .. })
2190            | Node::Emphasis(Emphasis { values, .. })
2191            | Node::TableRow(TableRow { values, .. })
2192            | Node::Fragment(Fragment { values, .. }) => match attr {
2193                attr_keys::VALUE => Some(AttrValue::String(values_to_string(values, &RenderOptions::default()))),
2194                attr_keys::VALUES | attr_keys::CHILDREN => Some(AttrValue::Array(values.clone())),
2195                _ => None,
2196            },
2197            Node::Break(_) | Node::HorizontalRule(_) | Node::Empty => None,
2198        }
2199    }
2200
2201    /// Sets the value of the specified attribute for the node, if supported.
2202    pub fn set_attr(&mut self, attr: &str, value: impl Into<AttrValue>) {
2203        let value = value.into();
2204        let value_str = value.as_string();
2205
2206        match self {
2207            Node::Footnote(f) => {
2208                if attr == attr_keys::IDENT {
2209                    f.ident = value_str;
2210                }
2211            }
2212            Node::Html(h) => {
2213                if attr == attr_keys::VALUE {
2214                    h.value = value_str;
2215                }
2216            }
2217            Node::Text(t) => {
2218                if attr == attr_keys::VALUE {
2219                    t.value = value_str;
2220                }
2221            }
2222            Node::Code(c) => match attr {
2223                attr_keys::VALUE => {
2224                    c.value = value_str;
2225                }
2226                attr_keys::LANG | "language" => {
2227                    c.lang = if value_str.is_empty() { None } else { Some(value_str) };
2228                }
2229                attr_keys::META => {
2230                    c.meta = if value_str.is_empty() { None } else { Some(value_str) };
2231                }
2232                attr_keys::FENCE => {
2233                    c.fence = match value {
2234                        AttrValue::Boolean(b) => b,
2235                        _ => value_str == "true",
2236                    };
2237                }
2238                _ => (),
2239            },
2240            Node::CodeInline(ci) => {
2241                if attr == attr_keys::VALUE {
2242                    ci.value = value_str.into();
2243                }
2244            }
2245            Node::MathInline(mi) => {
2246                if attr == attr_keys::VALUE {
2247                    mi.value = value_str.into();
2248                }
2249            }
2250            Node::Math(m) => {
2251                if attr == attr_keys::VALUE {
2252                    m.value = value_str;
2253                }
2254            }
2255            Node::Yaml(y) => {
2256                if attr == attr_keys::VALUE {
2257                    y.value = value_str;
2258                }
2259            }
2260            Node::Toml(t) => {
2261                if attr == attr_keys::VALUE {
2262                    t.value = value_str;
2263                }
2264            }
2265            Node::Image(i) => match attr {
2266                attr_keys::ALT => {
2267                    i.alt = value_str;
2268                }
2269                attr_keys::URL => {
2270                    i.url = value_str;
2271                }
2272                attr_keys::TITLE => {
2273                    i.title = if value_str.is_empty() { None } else { Some(value_str) };
2274                }
2275                _ => (),
2276            },
2277            Node::ImageRef(i) => match attr {
2278                attr_keys::ALT => {
2279                    i.alt = value_str;
2280                }
2281                attr_keys::IDENT => {
2282                    i.ident = value_str;
2283                }
2284                attr_keys::LABEL => {
2285                    i.label = if value_str.is_empty() { None } else { Some(value_str) };
2286                }
2287                _ => (),
2288            },
2289            Node::Link(l) => match attr {
2290                attr_keys::URL => {
2291                    l.url = Url::new(value_str);
2292                }
2293                attr_keys::TITLE => {
2294                    l.title = if value_str.is_empty() {
2295                        None
2296                    } else {
2297                        Some(Title::new(value_str))
2298                    };
2299                }
2300                _ => (),
2301            },
2302            Node::LinkRef(l) => match attr {
2303                attr_keys::IDENT => {
2304                    l.ident = value_str;
2305                }
2306                attr_keys::LABEL => {
2307                    l.label = if value_str.is_empty() { None } else { Some(value_str) };
2308                }
2309                _ => (),
2310            },
2311            Node::FootnoteRef(f) => match attr {
2312                attr_keys::IDENT => {
2313                    f.ident = value_str;
2314                }
2315                attr_keys::LABEL => {
2316                    f.label = if value_str.is_empty() { None } else { Some(value_str) };
2317                }
2318                _ => (),
2319            },
2320            Node::Definition(d) => match attr {
2321                attr_keys::IDENT => {
2322                    d.ident = value_str;
2323                }
2324                attr_keys::URL => {
2325                    d.url = Url::new(value_str);
2326                }
2327                attr_keys::TITLE => {
2328                    d.title = if value_str.is_empty() {
2329                        None
2330                    } else {
2331                        Some(Title::new(value_str))
2332                    };
2333                }
2334                attr_keys::LABEL => {
2335                    d.label = if value_str.is_empty() { None } else { Some(value_str) };
2336                }
2337                _ => (),
2338            },
2339            Node::Heading(h) => match attr {
2340                attr_keys::DEPTH | attr_keys::LEVEL => {
2341                    h.depth = match value {
2342                        AttrValue::Integer(i) => i as u8,
2343                        _ => value_str.parse::<u8>().unwrap_or(h.depth),
2344                    };
2345                }
2346                _ => (),
2347            },
2348            Node::List(l) => match attr {
2349                attr_keys::INDEX => {
2350                    l.index = match value {
2351                        AttrValue::Integer(i) => i as usize,
2352                        _ => value_str.parse::<usize>().unwrap_or(l.index),
2353                    };
2354                }
2355                attr_keys::LEVEL => {
2356                    l.level = match value {
2357                        AttrValue::Integer(i) => i as u8,
2358                        _ => value_str.parse::<u8>().unwrap_or(l.level),
2359                    };
2360                }
2361                attr_keys::ORDERED => {
2362                    l.ordered = match value {
2363                        AttrValue::Boolean(b) => b,
2364                        _ => value_str == "true",
2365                    };
2366                }
2367                attr_keys::CHECKED => {
2368                    l.checked = if value_str.is_empty() {
2369                        None
2370                    } else {
2371                        Some(match value {
2372                            AttrValue::Boolean(b) => b,
2373                            _ => value_str == "true",
2374                        })
2375                    };
2376                }
2377                _ => (),
2378            },
2379            Node::TableCell(c) => match attr {
2380                attr_keys::COLUMN => {
2381                    c.column = match value {
2382                        AttrValue::Integer(i) => i as usize,
2383                        _ => value_str.parse::<usize>().unwrap_or(c.column),
2384                    };
2385                }
2386                attr_keys::ROW => {
2387                    c.row = match value {
2388                        AttrValue::Integer(i) => i as usize,
2389                        _ => value_str.parse::<usize>().unwrap_or(c.row),
2390                    };
2391                }
2392                _ => (),
2393            },
2394            Node::TableAlign(th) => {
2395                if attr == attr_keys::ALIGN {
2396                    th.align = value_str.split(',').map(|s| s.trim().into()).collect();
2397                }
2398            }
2399            Node::MdxFlowExpression(m) => {
2400                if attr == attr_keys::VALUE {
2401                    m.value = value_str.into();
2402                }
2403            }
2404            Node::MdxTextExpression(m) => {
2405                if attr == attr_keys::VALUE {
2406                    m.value = value_str.into();
2407                }
2408            }
2409            Node::MdxJsEsm(m) => {
2410                if attr == attr_keys::VALUE {
2411                    m.value = value_str.into();
2412                }
2413            }
2414            Node::MdxJsxFlowElement(m) => {
2415                if attr == attr_keys::NAME {
2416                    m.name = if value_str.is_empty() { None } else { Some(value_str) };
2417                }
2418            }
2419            Node::MdxJsxTextElement(m) => {
2420                if attr == attr_keys::NAME {
2421                    m.name = if value_str.is_empty() {
2422                        None
2423                    } else {
2424                        Some(value_str.into())
2425                    };
2426                }
2427            }
2428            Node::Delete(_)
2429            | Node::Blockquote(_)
2430            | Node::Emphasis(_)
2431            | Node::Strong(_)
2432            | Node::TableRow(_)
2433            | Node::Break(_)
2434            | Node::HorizontalRule(_)
2435            | Node::Fragment(_)
2436            | Node::Empty => (),
2437            #[cfg(feature = "wikilink")]
2438            Node::WikiLink(w) => match attr {
2439                attr_keys::URL => w.target = value_str,
2440                attr_keys::VALUE => w.text = if value_str.is_empty() { None } else { Some(value_str) },
2441                _ => (),
2442            },
2443            #[cfg(feature = "callout")]
2444            Node::Callout(c) => match attr {
2445                attr_keys::KIND => c.kind = value_str,
2446                attr_keys::TITLE => c.title = if value_str.is_empty() { None } else { Some(value_str) },
2447                _ => (),
2448            },
2449            #[cfg(feature = "embed")]
2450            Node::Embed(e) => match attr {
2451                attr_keys::URL => e.target = value_str,
2452                attr_keys::VALUE => e.display = if value_str.is_empty() { None } else { Some(value_str) },
2453                _ => (),
2454            },
2455        }
2456    }
2457
2458    /// Tries to parse a `Blockquote`'s converted nodes as an Obsidian callout.
2459    ///
2460    /// Returns a `Callout` node when the first text starts with `[!TYPE]`, otherwise
2461    /// falls back to a plain `Blockquote`.
2462    #[cfg(feature = "callout")]
2463    fn try_parse_callout(values: Vec<Node>, position: Option<Position>) -> Node {
2464        // Peek without cloning so plain blockquotes pay no allocation cost.
2465        if !matches!(values.first(), Some(Node::Text(t)) if t.value.starts_with("[!")) {
2466            return Node::Blockquote(Blockquote { values, position });
2467        }
2468
2469        let mut iter = values.into_iter();
2470        // SAFETY: the peek above guarantees at least one Text node exists.
2471        let first_text = match iter.next().unwrap() {
2472            Node::Text(t) => t.value,
2473            _ => unreachable!(),
2474        };
2475
2476        let Some(rest) = first_text.strip_prefix("[!") else {
2477            unreachable!()
2478        };
2479
2480        let Some(bracket_end) = rest.find(']') else {
2481            // Malformed `[!…` without closing `]` — treat as plain blockquote.
2482            let mut values = vec![Node::Text(Text {
2483                value: first_text,
2484                position: None,
2485            })];
2486            values.extend(iter);
2487            return Node::Blockquote(Blockquote { values, position });
2488        };
2489
2490        let kind = rest[..bracket_end].to_string();
2491        let after_bracket = &rest[bracket_end + 1..];
2492
2493        let (title, remaining_body) = if let Some(nl) = after_bracket.find('\n') {
2494            let t = after_bracket[..nl].trim();
2495            (
2496                if t.is_empty() { None } else { Some(t.to_string()) },
2497                after_bracket[nl + 1..].to_string(),
2498            )
2499        } else {
2500            let t = after_bracket.trim();
2501            (if t.is_empty() { None } else { Some(t.to_string()) }, String::new())
2502        };
2503
2504        let body = if !remaining_body.trim().is_empty() {
2505            let rest_nodes: Vec<Node> = iter.collect();
2506            let mut b = Vec::with_capacity(rest_nodes.len() + 1);
2507            b.push(Node::Text(Text {
2508                value: remaining_body,
2509                position: None,
2510            }));
2511            b.extend(rest_nodes);
2512            b
2513        } else {
2514            iter.collect()
2515        };
2516
2517        Node::Callout(Callout {
2518            kind,
2519            title,
2520            values: body,
2521            position,
2522        })
2523    }
2524
2525    /// Returns true if any `Text` descendant contains `[[`, covering both
2526    /// embed (`![[`) and wikilink (`[[`) patterns.
2527    #[cfg(any(feature = "embed", feature = "wikilink"))]
2528    fn values_contain_links(values: &[Node]) -> bool {
2529        values.iter().any(|n| match n {
2530            Node::Text(t) => t.value.contains("[["),
2531            Node::Heading(h) => Self::values_contain_links(&h.values),
2532            Node::Blockquote(b) => Self::values_contain_links(&b.values),
2533            #[cfg(feature = "callout")]
2534            Node::Callout(c) => Self::values_contain_links(&c.values),
2535            Node::List(l) => Self::values_contain_links(&l.values),
2536            Node::Strong(s) => Self::values_contain_links(&s.values),
2537            Node::Emphasis(e) => Self::values_contain_links(&e.values),
2538            Node::Delete(d) => Self::values_contain_links(&d.values),
2539            Node::TableCell(tc) => Self::values_contain_links(&tc.values),
2540            Node::TableRow(tr) => Self::values_contain_links(&tr.values),
2541            Node::Footnote(f) => Self::values_contain_links(&f.values),
2542            _ => false,
2543        })
2544    }
2545
2546    /// Shared tree walker used by all expand functions. Recurses into container
2547    /// nodes and delegates `Text` node processing to `parse_into`.
2548    #[cfg(any(feature = "embed", feature = "wikilink"))]
2549    fn expand_with(nodes: Vec<Node>, parse_into: fn(&str, Option<Position>, &mut Vec<Node>)) -> Vec<Node> {
2550        let mut result = Vec::with_capacity(nodes.len());
2551        for node in nodes {
2552            Self::expand_with_into(node, &mut result, parse_into);
2553        }
2554        result
2555    }
2556
2557    #[cfg(any(feature = "embed", feature = "wikilink"))]
2558    fn expand_with_into(node: Node, out: &mut Vec<Node>, parse_into: fn(&str, Option<Position>, &mut Vec<Node>)) {
2559        match node {
2560            Node::Text(Text { ref value, .. }) if !value.contains("[[") => out.push(node),
2561            Node::Text(Text { value, position }) => parse_into(&value, position, out),
2562            Node::Heading(mut h) => {
2563                if Self::values_contain_links(&h.values) {
2564                    h.values = Self::expand_with(h.values, parse_into);
2565                }
2566                out.push(Node::Heading(h));
2567            }
2568            Node::Blockquote(mut b) => {
2569                if Self::values_contain_links(&b.values) {
2570                    b.values = Self::expand_with(b.values, parse_into);
2571                }
2572                out.push(Node::Blockquote(b));
2573            }
2574            #[cfg(feature = "callout")]
2575            Node::Callout(mut c) => {
2576                if Self::values_contain_links(&c.values) {
2577                    c.values = Self::expand_with(c.values, parse_into);
2578                }
2579                out.push(Node::Callout(c));
2580            }
2581            Node::List(mut l) => {
2582                if Self::values_contain_links(&l.values) {
2583                    l.values = Self::expand_with(l.values, parse_into);
2584                }
2585                out.push(Node::List(l));
2586            }
2587            Node::Strong(mut s) => {
2588                if Self::values_contain_links(&s.values) {
2589                    s.values = Self::expand_with(s.values, parse_into);
2590                }
2591                out.push(Node::Strong(s));
2592            }
2593            Node::Emphasis(mut e) => {
2594                if Self::values_contain_links(&e.values) {
2595                    e.values = Self::expand_with(e.values, parse_into);
2596                }
2597                out.push(Node::Emphasis(e));
2598            }
2599            Node::Delete(mut d) => {
2600                if Self::values_contain_links(&d.values) {
2601                    d.values = Self::expand_with(d.values, parse_into);
2602                }
2603                out.push(Node::Delete(d));
2604            }
2605            Node::TableCell(mut tc) => {
2606                if Self::values_contain_links(&tc.values) {
2607                    tc.values = Self::expand_with(tc.values, parse_into);
2608                }
2609                out.push(Node::TableCell(tc));
2610            }
2611            Node::TableRow(mut tr) => {
2612                if Self::values_contain_links(&tr.values) {
2613                    tr.values = Self::expand_with(tr.values, parse_into);
2614                }
2615                out.push(Node::TableRow(tr));
2616            }
2617            Node::Footnote(mut f) => {
2618                if Self::values_contain_links(&f.values) {
2619                    f.values = Self::expand_with(f.values, parse_into);
2620                }
2621                out.push(Node::Footnote(f));
2622            }
2623            other => out.push(other),
2624        }
2625    }
2626
2627    /// Recursively scans a node list and converts `![[target]]` / `![[target|display]]`
2628    /// patterns inside `Text` nodes into `Embed` nodes.
2629    #[cfg(feature = "embed")]
2630    pub fn expand_embeds(nodes: Vec<Node>) -> Vec<Node> {
2631        Self::expand_with(nodes, Self::parse_embeds_into)
2632    }
2633
2634    /// Splits a text string on `![[...]]` patterns, pushing a mix of `Text`
2635    /// and `Embed` nodes into `out`.
2636    #[cfg(feature = "embed")]
2637    fn parse_embeds_into(text: &str, position: Option<Position>, out: &mut Vec<Node>) {
2638        let mut last_end = 0;
2639        let mut search_from = 0;
2640        let mut found_any = false;
2641
2642        while let Some(bang_rel) = text[search_from..].find("![[") {
2643            let bang = search_from + bang_rel;
2644            let inner_start = bang + 3;
2645
2646            let Some(close_rel) = text[inner_start..].find("]]") else {
2647                search_from = bang + 1;
2648                continue;
2649            };
2650            let close = inner_start + close_rel;
2651            let content = &text[inner_start..close];
2652
2653            if content.contains('[') || content.contains(']') {
2654                search_from = bang + 1;
2655                continue;
2656            }
2657
2658            found_any = true;
2659            if last_end < bang {
2660                out.push(Node::Text(Text {
2661                    value: text[last_end..bang].to_string(),
2662                    position: position.clone(),
2663                }));
2664            }
2665            let (target, display) = if let Some(pipe) = content.find('|') {
2666                (
2667                    content[..pipe].trim().to_string(),
2668                    Some(content[pipe + 1..].trim().to_string()),
2669                )
2670            } else {
2671                (content.trim().to_string(), None)
2672            };
2673            out.push(Node::Embed(Embed {
2674                target,
2675                display,
2676                position: position.clone(),
2677            }));
2678            last_end = close + 2;
2679            search_from = close + 2;
2680        }
2681
2682        if !found_any {
2683            out.push(Node::Text(Text {
2684                value: text.to_string(),
2685                position,
2686            }));
2687            return;
2688        }
2689
2690        if last_end < text.len() {
2691            out.push(Node::Text(Text {
2692                value: text[last_end..].to_string(),
2693                position,
2694            }));
2695        }
2696    }
2697
2698    /// Recursively scans a node list and converts `[[target]]` / `[[target|text]]`
2699    /// patterns inside `Text` nodes into `WikiLink` nodes.
2700    #[cfg(feature = "wikilink")]
2701    pub fn expand_wikilinks(nodes: Vec<Node>) -> Vec<Node> {
2702        Self::expand_with(nodes, Self::parse_wikilinks_into)
2703    }
2704
2705    /// Splits a text string on `[[...]]` patterns, pushing a mix of `Text`
2706    /// and `WikiLink` nodes into `out`.
2707    #[cfg(feature = "wikilink")]
2708    fn parse_wikilinks_into(text: &str, position: Option<Position>, out: &mut Vec<Node>) {
2709        let mut last_end = 0;
2710        let mut search_from = 0;
2711        let mut found_any = false;
2712
2713        while let Some(open_rel) = text[search_from..].find("[[") {
2714            let open = search_from + open_rel;
2715            let inner_start = open + 2;
2716
2717            let Some(close_rel) = text[inner_start..].find("]]") else {
2718                break;
2719            };
2720            let close = inner_start + close_rel;
2721            let content = &text[inner_start..close];
2722
2723            if content.contains('[') || content.contains(']') {
2724                // nested brackets — skip past this '[' and retry
2725                search_from = open + 1;
2726                continue;
2727            }
2728
2729            found_any = true;
2730            if last_end < open {
2731                out.push(Node::Text(Text {
2732                    value: text[last_end..open].to_string(),
2733                    position: position.clone(),
2734                }));
2735            }
2736            let (target, text_part) = if let Some(pipe) = content.find('|') {
2737                (content[..pipe].trim(), Some(content[pipe + 1..].trim()))
2738            } else {
2739                (content.trim(), None)
2740            };
2741            out.push(Node::WikiLink(WikiLink {
2742                target: target.to_string(),
2743                text: text_part.map(|t| t.to_string()),
2744                position: position.clone(),
2745            }));
2746            last_end = close + 2;
2747            search_from = close + 2;
2748        }
2749
2750        if !found_any {
2751            out.push(Node::Text(Text {
2752                value: text.to_string(),
2753                position,
2754            }));
2755            return;
2756        }
2757
2758        if last_end < text.len() {
2759            out.push(Node::Text(Text {
2760                value: text[last_end..].to_string(),
2761                position,
2762            }));
2763        }
2764    }
2765
2766    /// Splits a text string on `[[...]]` patterns, returning a mix of `Text`
2767    /// and `WikiLink` nodes. Used only in tests.
2768    #[cfg(all(feature = "wikilink", test))]
2769    fn parse_wikilinks_in_text(text: &str, position: Option<Position>) -> Vec<Node> {
2770        let mut result = Vec::new();
2771        Self::parse_wikilinks_into(text, position, &mut result);
2772        result
2773    }
2774
2775    /// Recursively scans a node list and converts both `![[target]]` (embed) and
2776    /// `[[target]]` (wikilink) patterns in a single tree traversal. More efficient
2777    /// than running `expand_embeds` followed by `expand_wikilinks` separately, which
2778    /// traverses the tree and scans each `Text` node twice.
2779    #[cfg(all(feature = "embed", feature = "wikilink"))]
2780    pub fn expand_inline_links(nodes: Vec<Node>) -> Vec<Node> {
2781        Self::expand_with(nodes, Self::parse_inline_links_into)
2782    }
2783
2784    /// Splits a text string on `![[...]]` and `[[...]]` patterns in a single pass,
2785    /// pushing a mix of `Text`, `Embed`, and `WikiLink` nodes into `out`.
2786    /// A `[[` immediately preceded by `!` is treated as an embed; otherwise as a wikilink.
2787    #[cfg(all(feature = "embed", feature = "wikilink"))]
2788    fn parse_inline_links_into(text: &str, position: Option<Position>, out: &mut Vec<Node>) {
2789        let mut last_end = 0;
2790        let mut search_from = 0;
2791        let mut found_any = false;
2792
2793        while let Some(open_rel) = text[search_from..].find("[[") {
2794            let open = search_from + open_rel;
2795            let inner_start = open + 2;
2796
2797            let Some(close_rel) = text[inner_start..].find("]]") else {
2798                break;
2799            };
2800            let close = inner_start + close_rel;
2801            let content = &text[inner_start..close];
2802
2803            if content.contains('[') || content.contains(']') {
2804                search_from = open + 1;
2805                continue;
2806            }
2807
2808            let is_embed = text.as_bytes()[..open].last() == Some(&b'!');
2809            let token_start = if is_embed { open - 1 } else { open };
2810
2811            found_any = true;
2812            if last_end < token_start {
2813                out.push(Node::Text(Text {
2814                    value: text[last_end..token_start].to_string(),
2815                    position: position.clone(),
2816                }));
2817            }
2818
2819            let (target, opt_part) = if let Some(pipe) = content.find('|') {
2820                (content[..pipe].trim(), Some(content[pipe + 1..].trim()))
2821            } else {
2822                (content.trim(), None)
2823            };
2824
2825            if is_embed {
2826                out.push(Node::Embed(Embed {
2827                    target: target.to_string(),
2828                    display: opt_part.map(|s| s.to_string()),
2829                    position: position.clone(),
2830                }));
2831            } else {
2832                out.push(Node::WikiLink(WikiLink {
2833                    target: target.to_string(),
2834                    text: opt_part.map(|s| s.to_string()),
2835                    position: position.clone(),
2836                }));
2837            }
2838
2839            last_end = close + 2;
2840            search_from = close + 2;
2841        }
2842
2843        if !found_any {
2844            out.push(Node::Text(Text {
2845                value: text.to_string(),
2846                position,
2847            }));
2848            return;
2849        }
2850
2851        if last_end < text.len() {
2852            out.push(Node::Text(Text {
2853                value: text[last_end..].to_string(),
2854                position,
2855            }));
2856        }
2857    }
2858
2859    pub(crate) fn from_mdast_node(node: mdast::Node) -> Vec<Node> {
2860        match node.clone() {
2861            mdast::Node::Root(root) => root
2862                .children
2863                .into_iter()
2864                .flat_map(Self::from_mdast_node)
2865                .collect::<Vec<_>>(),
2866            mdast::Node::ListItem(list_item) => list_item
2867                .children
2868                .into_iter()
2869                .flat_map(Self::from_mdast_node)
2870                .collect::<Vec<_>>(),
2871            mdast::Node::List(list) => Self::mdast_list_items(&list, 0),
2872            mdast::Node::Table(table) => table
2873                .children
2874                .iter()
2875                .enumerate()
2876                .flat_map(|(row, n)| {
2877                    if let mdast::Node::TableRow(table_row) = n {
2878                        itertools::concat(vec![
2879                            table_row
2880                                .children
2881                                .iter()
2882                                .enumerate()
2883                                .flat_map(|(column, node)| {
2884                                    if let mdast::Node::TableCell(_) = node {
2885                                        vec![Self::TableCell(TableCell {
2886                                            row,
2887                                            column,
2888                                            values: Self::mdast_children_to_node(node.clone()),
2889                                            position: node.position().map(|p| p.clone().into()),
2890                                        })]
2891                                    } else {
2892                                        Vec::new()
2893                                    }
2894                                })
2895                                .collect(),
2896                            if row == 0 {
2897                                vec![Self::TableAlign(TableAlign {
2898                                    align: table.align.iter().map(|a| (*a).into()).collect::<Vec<_>>(),
2899                                    position: n.position().map(|p| Position {
2900                                        start: Point {
2901                                            line: p.start.line + 1,
2902                                            column: 1,
2903                                        },
2904                                        end: Point {
2905                                            line: p.start.line + 1,
2906                                            column: 1,
2907                                        },
2908                                    }),
2909                                })]
2910                            } else {
2911                                Vec::new()
2912                            },
2913                        ])
2914                    } else {
2915                        Vec::new()
2916                    }
2917                })
2918                .collect(),
2919            mdast::Node::Code(mdast::Code {
2920                value,
2921                position,
2922                lang,
2923                meta,
2924                ..
2925            }) => match lang {
2926                Some(lang) => {
2927                    vec![Self::Code(Code {
2928                        value,
2929                        lang: Some(lang),
2930                        position: position.map(|p| p.clone().into()),
2931                        meta,
2932                        fence: true,
2933                    })]
2934                }
2935                None => {
2936                    let line_count = position
2937                        .as_ref()
2938                        .map(|p| p.end.line - p.start.line + 1)
2939                        .unwrap_or_default();
2940                    let fence = value.lines().count() != line_count;
2941
2942                    vec![Self::Code(Code {
2943                        value,
2944                        lang,
2945                        position: position.map(|p| p.clone().into()),
2946                        meta,
2947                        fence,
2948                    })]
2949                }
2950            },
2951            mdast::Node::Blockquote(mdast::Blockquote { position, .. }) => {
2952                let values = Self::mdast_children_to_node(node);
2953                let pos = position.map(|p| p.clone().into());
2954                #[cfg(feature = "callout")]
2955                {
2956                    vec![Self::try_parse_callout(values, pos)]
2957                }
2958                #[cfg(not(feature = "callout"))]
2959                {
2960                    vec![Self::Blockquote(Blockquote { values, position: pos })]
2961                }
2962            }
2963            mdast::Node::Definition(mdast::Definition {
2964                url,
2965                title,
2966                identifier,
2967                label,
2968                position,
2969                ..
2970            }) => {
2971                vec![Self::Definition(Definition {
2972                    ident: identifier,
2973                    url: Url(url),
2974                    label,
2975                    title: title.map(Title),
2976                    position: position.map(|p| p.clone().into()),
2977                })]
2978            }
2979            mdast::Node::Heading(mdast::Heading { depth, position, .. }) => {
2980                vec![Self::Heading(Heading {
2981                    values: Self::mdast_children_to_node(node),
2982                    depth,
2983                    position: position.map(|p| p.clone().into()),
2984                })]
2985            }
2986            mdast::Node::Break(mdast::Break { position }) => {
2987                vec![Self::Break(Break {
2988                    position: position.map(|p| p.clone().into()),
2989                })]
2990            }
2991            mdast::Node::Delete(mdast::Delete { position, .. }) => {
2992                vec![Self::Delete(Delete {
2993                    values: Self::mdast_children_to_node(node),
2994                    position: position.map(|p| p.clone().into()),
2995                })]
2996            }
2997            mdast::Node::Emphasis(mdast::Emphasis { position, .. }) => {
2998                vec![Self::Emphasis(Emphasis {
2999                    values: Self::mdast_children_to_node(node),
3000                    position: position.map(|p| p.clone().into()),
3001                })]
3002            }
3003            mdast::Node::Strong(mdast::Strong { position, .. }) => {
3004                vec![Self::Strong(Strong {
3005                    values: Self::mdast_children_to_node(node),
3006                    position: position.map(|p| p.clone().into()),
3007                })]
3008            }
3009            mdast::Node::ThematicBreak(mdast::ThematicBreak { position, .. }) => {
3010                vec![Self::HorizontalRule(HorizontalRule {
3011                    position: position.map(|p| p.clone().into()),
3012                })]
3013            }
3014            mdast::Node::Html(mdast::Html { value, position }) => {
3015                vec![Self::Html(Html {
3016                    value,
3017                    position: position.map(|p| p.clone().into()),
3018                })]
3019            }
3020            mdast::Node::Yaml(mdast::Yaml { value, position }) => {
3021                vec![Self::Yaml(Yaml {
3022                    value,
3023                    position: position.map(|p| p.clone().into()),
3024                })]
3025            }
3026            mdast::Node::Toml(mdast::Toml { value, position }) => {
3027                vec![Self::Toml(Toml {
3028                    value,
3029                    position: position.map(|p| p.clone().into()),
3030                })]
3031            }
3032            mdast::Node::Image(mdast::Image {
3033                alt,
3034                url,
3035                title,
3036                position,
3037            }) => {
3038                vec![Self::Image(Image {
3039                    alt,
3040                    url,
3041                    title,
3042                    position: position.map(|p| p.clone().into()),
3043                })]
3044            }
3045            mdast::Node::ImageReference(mdast::ImageReference {
3046                alt,
3047                identifier,
3048                label,
3049                position,
3050                ..
3051            }) => {
3052                vec![Self::ImageRef(ImageRef {
3053                    alt,
3054                    ident: identifier,
3055                    label,
3056                    position: position.map(|p| p.clone().into()),
3057                })]
3058            }
3059            mdast::Node::InlineCode(mdast::InlineCode { value, position, .. }) => {
3060                vec![Self::CodeInline(CodeInline {
3061                    value: value.into(),
3062                    position: position.map(|p| p.clone().into()),
3063                })]
3064            }
3065            mdast::Node::InlineMath(mdast::InlineMath { value, position }) => {
3066                vec![Self::MathInline(MathInline {
3067                    value: value.into(),
3068                    position: position.map(|p| p.clone().into()),
3069                })]
3070            }
3071            mdast::Node::Link(mdast::Link {
3072                title,
3073                url,
3074                position,
3075                children,
3076                ..
3077            }) => {
3078                let converted: Vec<Node> = children.into_iter().flat_map(Self::from_mdast_node).collect();
3079                // Flatten nested links that arise from GFM autolink literal parsing.
3080                // When the link text is a bare URL (e.g. `[https://x](https://x)`),
3081                // markdown-rs parses the inner text as another Link node with the same
3082                // URL, which causes double-nesting on re-serialisation.  Unwrap any
3083                // such inner link whose URL matches the outer one.
3084                let values = converted
3085                    .into_iter()
3086                    .flat_map(|child| match child {
3087                        Self::Link(Link {
3088                            url: ref inner_url,
3089                            ref values,
3090                            ..
3091                        }) if inner_url.0 == url => values.clone(),
3092                        other => vec![other],
3093                    })
3094                    .collect();
3095                vec![Self::Link(Link {
3096                    url: Url(url),
3097                    title: title.map(Title),
3098                    values,
3099                    position: position.map(|p| p.clone().into()),
3100                })]
3101            }
3102            mdast::Node::LinkReference(mdast::LinkReference {
3103                identifier,
3104                label,
3105                position,
3106                children,
3107                ..
3108            }) => {
3109                vec![Self::LinkRef(LinkRef {
3110                    ident: identifier,
3111                    values: children.into_iter().flat_map(Self::from_mdast_node).collect::<Vec<_>>(),
3112                    label,
3113                    position: position.map(|p| p.clone().into()),
3114                })]
3115            }
3116            mdast::Node::Math(mdast::Math { value, position, .. }) => {
3117                vec![Self::Math(Math {
3118                    value,
3119                    position: position.map(|p| p.clone().into()),
3120                })]
3121            }
3122            mdast::Node::FootnoteDefinition(mdast::FootnoteDefinition {
3123                identifier,
3124                position,
3125                children,
3126                ..
3127            }) => {
3128                vec![Self::Footnote(Footnote {
3129                    ident: identifier,
3130                    values: children.into_iter().flat_map(Self::from_mdast_node).collect::<Vec<_>>(),
3131                    position: position.map(|p| p.clone().into()),
3132                })]
3133            }
3134            mdast::Node::FootnoteReference(mdast::FootnoteReference {
3135                identifier,
3136                label,
3137                position,
3138                ..
3139            }) => {
3140                vec![Self::FootnoteRef(FootnoteRef {
3141                    ident: identifier,
3142                    label,
3143                    position: position.map(|p| p.clone().into()),
3144                })]
3145            }
3146            mdast::Node::MdxFlowExpression(mdx) => {
3147                vec![Self::MdxFlowExpression(MdxFlowExpression {
3148                    value: mdx.value.into(),
3149                    position: mdx.position.map(|position| position.into()),
3150                })]
3151            }
3152            mdast::Node::MdxJsxFlowElement(mdx) => {
3153                vec![Self::MdxJsxFlowElement(MdxJsxFlowElement {
3154                    children: mdx
3155                        .children
3156                        .into_iter()
3157                        .flat_map(Self::from_mdast_node)
3158                        .collect::<Vec<_>>(),
3159                    position: mdx.position.map(|p| p.clone().into()),
3160                    name: mdx.name,
3161                    attributes: mdx
3162                        .attributes
3163                        .iter()
3164                        .map(|attr| match attr {
3165                            mdast::AttributeContent::Expression(mdast::MdxJsxExpressionAttribute { value, .. }) => {
3166                                MdxAttributeContent::Expression(value.into())
3167                            }
3168                            mdast::AttributeContent::Property(mdast::MdxJsxAttribute { value, name, .. }) => {
3169                                MdxAttributeContent::Property(MdxJsxAttribute {
3170                                    name: name.into(),
3171                                    value: value.as_ref().map(|value| match value {
3172                                        mdast::AttributeValue::Literal(value) => {
3173                                            MdxAttributeValue::Literal(value.into())
3174                                        }
3175                                        mdast::AttributeValue::Expression(mdast::AttributeValueExpression {
3176                                            value,
3177                                            ..
3178                                        }) => MdxAttributeValue::Expression(value.into()),
3179                                    }),
3180                                })
3181                            }
3182                        })
3183                        .collect(),
3184                })]
3185            }
3186            mdast::Node::MdxJsxTextElement(mdx) => {
3187                vec![Self::MdxJsxTextElement(MdxJsxTextElement {
3188                    children: mdx
3189                        .children
3190                        .into_iter()
3191                        .flat_map(Self::from_mdast_node)
3192                        .collect::<Vec<_>>(),
3193                    position: mdx.position.map(|p| p.clone().into()),
3194                    name: mdx.name.map(|name| name.into()),
3195                    attributes: mdx
3196                        .attributes
3197                        .iter()
3198                        .map(|attr| match attr {
3199                            mdast::AttributeContent::Expression(mdast::MdxJsxExpressionAttribute { value, .. }) => {
3200                                MdxAttributeContent::Expression(value.into())
3201                            }
3202                            mdast::AttributeContent::Property(mdast::MdxJsxAttribute { value, name, .. }) => {
3203                                MdxAttributeContent::Property(MdxJsxAttribute {
3204                                    name: name.into(),
3205                                    value: value.as_ref().map(|value| match value {
3206                                        mdast::AttributeValue::Literal(value) => {
3207                                            MdxAttributeValue::Literal(value.into())
3208                                        }
3209                                        mdast::AttributeValue::Expression(mdast::AttributeValueExpression {
3210                                            value,
3211                                            ..
3212                                        }) => MdxAttributeValue::Expression(value.into()),
3213                                    }),
3214                                })
3215                            }
3216                        })
3217                        .collect(),
3218                })]
3219            }
3220            mdast::Node::MdxTextExpression(mdx) => {
3221                vec![Self::MdxTextExpression(MdxTextExpression {
3222                    value: mdx.value.into(),
3223                    position: mdx.position.map(|position| position.into()),
3224                })]
3225            }
3226            mdast::Node::MdxjsEsm(mdx) => {
3227                vec![Self::MdxJsEsm(MdxJsEsm {
3228                    value: mdx.value.into(),
3229                    position: mdx.position.map(|position| position.into()),
3230                })]
3231            }
3232            mdast::Node::Text(mdast::Text { position, value, .. }) => {
3233                vec![Self::Text(Text {
3234                    value,
3235                    position: position.map(|p| p.clone().into()),
3236                })]
3237            }
3238            mdast::Node::Paragraph(mdast::Paragraph { children, .. }) => {
3239                children.into_iter().flat_map(Self::from_mdast_node).collect::<Vec<_>>()
3240            }
3241            _ => Vec::new(),
3242        }
3243    }
3244
3245    fn mdast_children_to_node(node: mdast::Node) -> Vec<Node> {
3246        node.children()
3247            .map(|children| {
3248                children
3249                    .iter()
3250                    .flat_map(|v| Self::from_mdast_node(v.clone()))
3251                    .collect::<Vec<_>>()
3252            })
3253            .unwrap_or_else(|| vec![EMPTY_NODE])
3254    }
3255
3256    fn mdast_list_items(list: &mdast::List, level: Level) -> Vec<Node> {
3257        list.children
3258            .iter()
3259            .flat_map(|n| {
3260                if let mdast::Node::ListItem(list_item) = n {
3261                    let values = Self::from_mdast_node(n.clone())
3262                        .into_iter()
3263                        .filter(|value| !matches!(value, Self::List(_)))
3264                        .collect::<Vec<_>>();
3265                    let position = if values.is_empty() {
3266                        n.position().map(|p| p.clone().into())
3267                    } else {
3268                        let first_pos = values.first().and_then(|v| v.position());
3269                        let last_pos = values.last().and_then(|v| v.position());
3270                        match (first_pos, last_pos) {
3271                            (Some(start), Some(end)) => Some(Position {
3272                                start: start.start.clone(),
3273                                end: end.end.clone(),
3274                            }),
3275                            _ => n.position().map(|p| p.clone().into()),
3276                        }
3277                    };
3278
3279                    itertools::concat(vec![
3280                        vec![Self::List(List {
3281                            level,
3282                            index: 0,
3283                            ordered: list.ordered,
3284                            checked: list_item.checked,
3285                            values,
3286                            position,
3287                        })],
3288                        list_item
3289                            .children
3290                            .iter()
3291                            .flat_map(|node| {
3292                                if let mdast::Node::List(sub_list) = node {
3293                                    Self::mdast_list_items(sub_list, level + 1)
3294                                } else if let mdast::Node::ListItem(list_item) = node {
3295                                    let values = Self::from_mdast_node(n.clone())
3296                                        .into_iter()
3297                                        .filter(|value| !matches!(value, Self::List(_)))
3298                                        .collect::<Vec<_>>();
3299                                    let position = if values.is_empty() {
3300                                        n.position().map(|p| p.clone().into())
3301                                    } else {
3302                                        let first_pos = values.first().and_then(|v| v.position());
3303                                        let last_pos = values.last().and_then(|v| v.position());
3304                                        match (first_pos, last_pos) {
3305                                            (Some(start), Some(end)) => Some(Position {
3306                                                start: start.start.clone(),
3307                                                end: end.end.clone(),
3308                                            }),
3309                                            _ => n.position().map(|p| p.clone().into()),
3310                                        }
3311                                    };
3312                                    vec![Self::List(List {
3313                                        level: level + 1,
3314                                        index: 0,
3315                                        ordered: list.ordered,
3316                                        checked: list_item.checked,
3317                                        values,
3318                                        position,
3319                                    })]
3320                                } else {
3321                                    Vec::new()
3322                                }
3323                            })
3324                            .collect(),
3325                    ])
3326                } else if let mdast::Node::List(sub_list) = n {
3327                    Self::mdast_list_items(sub_list, level + 1)
3328                } else {
3329                    Vec::new()
3330                }
3331            })
3332            .enumerate()
3333            .filter_map(|(i, node)| match node {
3334                Self::List(List {
3335                    level,
3336                    index: _,
3337                    ordered,
3338                    checked,
3339                    values,
3340                    position,
3341                }) => Some(Self::List(List {
3342                    level,
3343                    index: i,
3344                    ordered,
3345                    checked,
3346                    values,
3347                    position,
3348                })),
3349                _ => None,
3350            })
3351            .collect()
3352    }
3353
3354    fn mdx_attribute_content_to_string(attr: MdxAttributeContent) -> SmolStr {
3355        match attr {
3356            MdxAttributeContent::Expression(value) => format!("{{{}}}", value).into(),
3357            MdxAttributeContent::Property(property) => match property.value {
3358                Some(value) => match value {
3359                    MdxAttributeValue::Expression(value) => format!("{}={{{}}}", property.name, value).into(),
3360                    MdxAttributeValue::Literal(literal) => format!("{}=\"{}\"", property.name, literal).into(),
3361                },
3362                None => property.name,
3363            },
3364        }
3365    }
3366}
3367
3368pub(crate) fn values_to_string(values: &[Node], options: &RenderOptions) -> String {
3369    render_values(values, options, &ColorTheme::PLAIN)
3370}
3371
3372pub(crate) fn render_values(values: &[Node], options: &RenderOptions, theme: &ColorTheme<'_>) -> String {
3373    let mut pre_position: Option<Position> = None;
3374    values
3375        .iter()
3376        .map(|value| {
3377            if let Some(pos) = value.position() {
3378                let new_line_count = pre_position
3379                    .as_ref()
3380                    .map(|p: &Position| pos.start.line - p.end.line)
3381                    .unwrap_or_default();
3382
3383                let space = if new_line_count > 0
3384                    && pre_position
3385                        .as_ref()
3386                        .map(|p| pos.start.line > p.end.line)
3387                        .unwrap_or_default()
3388                {
3389                    " ".repeat(pos.start.column.saturating_sub(1))
3390                } else {
3391                    "".to_string()
3392                };
3393
3394                pre_position = Some(pos);
3395
3396                if space.is_empty() {
3397                    format!(
3398                        "{}{}",
3399                        "\n".repeat(new_line_count),
3400                        value.render_with_theme(options, theme)
3401                    )
3402                } else {
3403                    format!(
3404                        "{}{}",
3405                        "\n".repeat(new_line_count),
3406                        value
3407                            .render_with_theme(options, theme)
3408                            .lines()
3409                            .map(|line| format!("{}{}", space, line))
3410                            .join("\n")
3411                    )
3412                }
3413            } else {
3414                pre_position = None;
3415                value.render_with_theme(options, theme)
3416            }
3417        })
3418        .collect::<String>()
3419}
3420
3421/// Like `render_values` but without column-offset–based indentation.
3422///
3423/// Inside blockquotes and callouts, node positions include the `> ` prefix offset,
3424/// so applying column-based spacing causes double-indentation. This variant preserves
3425/// blank lines between values (via newline counting) but ignores the column position.
3426pub(crate) fn render_values_block(values: &[Node], options: &RenderOptions, theme: &ColorTheme<'_>) -> String {
3427    let mut pre_position: Option<Position> = None;
3428    values
3429        .iter()
3430        .map(|value| {
3431            if let Some(pos) = value.position() {
3432                let new_line_count = pre_position
3433                    .as_ref()
3434                    .map(|p: &Position| pos.start.line - p.end.line)
3435                    .unwrap_or_default();
3436                pre_position = Some(pos);
3437                format!(
3438                    "{}{}",
3439                    "\n".repeat(new_line_count),
3440                    value.render_with_theme(options, theme)
3441                )
3442            } else {
3443                pre_position = None;
3444                value.render_with_theme(options, theme)
3445            }
3446        })
3447        .collect::<String>()
3448}
3449
3450fn values_to_value(values: Vec<Node>) -> String {
3451    values.iter().map(|value| value.value()).collect::<String>()
3452}
3453
3454#[cfg(test)]
3455mod tests {
3456    use super::*;
3457    use rstest::rstest;
3458
3459    #[rstest]
3460    #[case::text(Node::Text(Text{value: "".to_string(), position: None}),
3461           "test".to_string(),
3462           Node::Text(Text{value: "test".to_string(), position: None }))]
3463    #[case::blockquote(Node::Blockquote(Blockquote{values: vec!["test".to_string().into()], position: None }),
3464           "test".to_string(),
3465           Node::Blockquote(Blockquote{values: vec!["test".to_string().into()], position: None }))]
3466    #[case::delete(Node::Delete(Delete{values: vec!["test".to_string().into()], position: None }),
3467           "test".to_string(),
3468           Node::Delete(Delete{values: vec!["test".to_string().into()], position: None }))]
3469    #[case::emphasis(Node::Emphasis(Emphasis{values: vec!["test".to_string().into()], position: None }),
3470           "test".to_string(),
3471           Node::Emphasis(Emphasis{values: vec!["test".to_string().into()], position: None }))]
3472    #[case::strong(Node::Strong(Strong{values: vec!["test".to_string().into()], position: None }),
3473           "test".to_string(),
3474           Node::Strong(Strong{values: vec!["test".to_string().into()], position: None }))]
3475    #[case::heading(Node::Heading(Heading {depth: 1, values: vec!["test".to_string().into()], position: None }),
3476           "test".to_string(),
3477           Node::Heading(Heading{depth: 1, values: vec!["test".to_string().into()], position: None }))]
3478    #[case::link(Node::Link(Link {url: Url::new("test".to_string()), values: Vec::new(), title: None, position: None }),
3479           "test".to_string(),
3480           Node::Link(Link{url: Url::new("test".to_string()), values: Vec::new(), title: None, position: None }))]
3481    #[case::image(Node::Image(Image {alt: "test".to_string(), url: "test".to_string(), title: None, position: None }),
3482           "test".to_string(),
3483           Node::Image(Image{alt: "test".to_string(), url: "test".to_string(), title: None, position: None }))]
3484    #[case::code(Node::Code(Code {value: "test".to_string(), lang: None, fence: true, meta: None, position: None }),
3485           "test".to_string(),
3486           Node::Code(Code{value: "test".to_string(), lang: None, fence: true, meta: None, position: None }))]
3487    #[case::footnote_ref(Node::FootnoteRef(FootnoteRef {ident: "test".to_string(), label: None, position: None }),
3488           "test".to_string(),
3489           Node::FootnoteRef(FootnoteRef{ident: "test".to_string(), label: Some("test".to_string()), position: None }))]
3490    #[case::footnote(Node::Footnote(Footnote {ident: "test".to_string(), values: Vec::new(), position: None }),
3491           "test".to_string(),
3492           Node::Footnote(Footnote{ident: "test".to_string(), values: Vec::new(), position: None }))]
3493    #[case::list(Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec!["test".to_string().into()], position: None }),
3494           "test".to_string(),
3495           Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec!["test".to_string().into()], position: None }))]
3496    #[case::list(Node::List(List{index: 1, level: 1, checked: Some(true), ordered: false, values: vec!["test".to_string().into()], position: None }),
3497           "test".to_string(),
3498           Node::List(List{index: 1, level: 1, checked: Some(true), ordered: false, values: vec!["test".to_string().into()], position: None }))]
3499    #[case::list(Node::List(List{index: 2, level: 2, checked: Some(false), ordered: false, values: vec!["test".to_string().into()], position: None }),
3500           "test".to_string(),
3501           Node::List(List{index: 2, level: 2, checked: Some(false), ordered: false, values: vec!["test".to_string().into()], position: None }))]
3502    #[case::code_inline(Node::CodeInline(CodeInline{ value: "t".into(), position: None }),
3503           "test".to_string(),
3504           Node::CodeInline(CodeInline{ value: "test".into(), position: None }))]
3505    #[case::math_inline(Node::MathInline(MathInline{ value: "t".into(), position: None }),
3506           "test".to_string(),
3507           Node::MathInline(MathInline{ value: "test".into(), position: None }))]
3508    #[case::toml(Node::Toml(Toml{ value: "t".to_string(), position: None }),
3509           "test".to_string(),
3510           Node::Toml(Toml{ value: "test".to_string(), position: None }))]
3511    #[case::yaml(Node::Yaml(Yaml{ value: "t".to_string(), position: None }),
3512           "test".to_string(),
3513           Node::Yaml(Yaml{ value: "test".to_string(), position: None }))]
3514    #[case::html(Node::Html(Html{ value: "t".to_string(), position: None }),
3515           "test".to_string(),
3516           Node::Html(Html{ value: "test".to_string(), position: None }))]
3517    #[case::table_row(Node::TableRow(TableRow{ values: vec![
3518                        Node::TableCell(TableCell{values: vec!["test1".to_string().into()], row:0, column:1,  position: None}),
3519                        Node::TableCell(TableCell{values: vec!["test2".to_string().into()], row:0, column:2,  position: None})
3520                    ]
3521                    , position: None }),
3522           "test3,test4".to_string(),
3523           Node::TableRow(TableRow{ values: vec![
3524                        Node::TableCell(TableCell{values: vec!["test3".to_string().into()], row:0, column:1, position: None}),
3525                        Node::TableCell(TableCell{values: vec!["test4".to_string().into()], row:0, column:2, position: None})
3526                    ]
3527                    , position: None }))]
3528    #[case::table_cell(Node::TableCell(TableCell{values: vec!["test1".to_string().into()], row:0, column:1, position: None}),
3529            "test2".to_string(),
3530            Node::TableCell(TableCell{values: vec!["test2".to_string().into()], row:0, column:1, position: None}),)]
3531    #[case::link_ref(Node::LinkRef(LinkRef{ident: "test2".to_string(), values: vec![attr_keys::VALUE.to_string().into()], label: Some("test2".to_string()), position: None}),
3532            "test2".to_string(),
3533            Node::LinkRef(LinkRef{ident: "test2".to_string(), values: vec![attr_keys::VALUE.to_string().into()], label: Some("test2".to_string()), position: None}),)]
3534    #[case::image_ref(Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "test1".to_string(), label: None, position: None}),
3535            "test2".to_string(),
3536            Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "test2".to_string(), label: Some("test2".to_string()), position: None}),)]
3537    #[case::definition(Node::Definition(Definition{ url: Url::new(attr_keys::URL.to_string()), title: None, ident: "test1".to_string(), label: None, position: None}),
3538            "test2".to_string(),
3539            Node::Definition(Definition{url: Url::new("test2".to_string()), title: None, ident: "test1".to_string(), label: None, position: None}),)]
3540    #[case::break_(Node::Break(Break{ position: None}),
3541            "test".to_string(),
3542            Node::Break(Break{position: None}))]
3543    #[case::horizontal_rule(Node::HorizontalRule(HorizontalRule{ position: None}),
3544            "test".to_string(),
3545            Node::HorizontalRule(HorizontalRule{position: None}))]
3546    #[case::mdx_flow_expression(Node::MdxFlowExpression(MdxFlowExpression{value: "test".into(), position: None}),
3547           "updated".to_string(),
3548           Node::MdxFlowExpression(MdxFlowExpression{value: "updated".into(), position: None}))]
3549    #[case::mdx_text_expression(Node::MdxTextExpression(MdxTextExpression{value: "test".into(), position: None}),
3550           "updated".to_string(),
3551           Node::MdxTextExpression(MdxTextExpression{value: "updated".into(), position: None}))]
3552    #[case::mdx_js_esm(Node::MdxJsEsm(MdxJsEsm{value: "test".into(), position: None}),
3553           "updated".to_string(),
3554           Node::MdxJsEsm(MdxJsEsm{value: "updated".into(), position: None}))]
3555    #[case(Node::MdxJsxFlowElement(MdxJsxFlowElement{
3556            name: Some("div".to_string()),
3557            attributes: Vec::new(),
3558            children: vec!["test".to_string().into()],
3559            position: None
3560        }),
3561        "updated".to_string(),
3562        Node::MdxJsxFlowElement(MdxJsxFlowElement{
3563            name: Some("div".to_string()),
3564            attributes: Vec::new(),
3565            children: vec!["updated".to_string().into()],
3566            position: None
3567        }))]
3568    #[case::mdx_jsx_text_element(Node::MdxJsxTextElement(MdxJsxTextElement{
3569            name: Some("span".into()),
3570            attributes: Vec::new(),
3571            children: vec!["test".to_string().into()],
3572            position: None
3573        }),
3574        "updated".to_string(),
3575        Node::MdxJsxTextElement(MdxJsxTextElement{
3576            name: Some("span".into()),
3577            attributes: Vec::new(),
3578            children: vec!["updated".to_string().into()],
3579            position: None
3580        }))]
3581    #[case(Node::Math(Math{ value: "x^2".to_string(), position: None }),
3582           "test".to_string(),
3583           Node::Math(Math{ value: "test".to_string(), position: None }))]
3584    fn test_with_value(#[case] node: Node, #[case] input: String, #[case] expected: Node) {
3585        assert_eq!(node.with_value(input.as_str()), expected);
3586    }
3587
3588    #[rstest]
3589    #[case(Node::Blockquote(Blockquote{values: vec![
3590        Node::Text(Text{value: "first".to_string(), position: None}),
3591        Node::Text(Text{value: "second".to_string(), position: None})
3592    ], position: None}),
3593        "new",
3594        0,
3595        Node::Blockquote(Blockquote{values: vec![
3596            Node::Text(Text{value: "new".to_string(), position: None}),
3597            Node::Text(Text{value: "second".to_string(), position: None})
3598        ], position: None}))]
3599    #[case(Node::Blockquote(Blockquote{values: vec![
3600        Node::Text(Text{value: "first".to_string(), position: None}),
3601        Node::Text(Text{value: "second".to_string(), position: None})
3602    ], position: None}),
3603        "new",
3604        1,
3605        Node::Blockquote(Blockquote{values: vec![
3606            Node::Text(Text{value: "first".to_string(), position: None}),
3607            Node::Text(Text{value: "new".to_string(), position: None})
3608        ], position: None}))]
3609    #[case(Node::Delete(Delete{values: vec![
3610        Node::Text(Text{value: "first".to_string(), position: None}),
3611        Node::Text(Text{value: "second".to_string(), position: None})
3612    ], position: None}),
3613        "new",
3614        0,
3615        Node::Delete(Delete{values: vec![
3616            Node::Text(Text{value: "new".to_string(), position: None}),
3617            Node::Text(Text{value: "second".to_string(), position: None})
3618        ], position: None}))]
3619    #[case(Node::Emphasis(Emphasis{values: vec![
3620        Node::Text(Text{value: "first".to_string(), position: None}),
3621        Node::Text(Text{value: "second".to_string(), position: None})
3622    ], position: None}),
3623        "new",
3624        1,
3625        Node::Emphasis(Emphasis{values: vec![
3626            Node::Text(Text{value: "first".to_string(), position: None}),
3627            Node::Text(Text{value: "new".to_string(), position: None})
3628        ], position: None}))]
3629    #[case(Node::Strong(Strong{values: vec![
3630        Node::Text(Text{value: "first".to_string(), position: None}),
3631        Node::Text(Text{value: "second".to_string(), position: None})
3632    ], position: None}),
3633        "new",
3634        0,
3635        Node::Strong(Strong{values: vec![
3636            Node::Text(Text{value: "new".to_string(), position: None}),
3637            Node::Text(Text{value: "second".to_string(), position: None})
3638        ], position: None}))]
3639    #[case(Node::Heading(Heading{depth: 1, values: vec![
3640        Node::Text(Text{value: "first".to_string(), position: None}),
3641        Node::Text(Text{value: "second".to_string(), position: None})
3642    ], position: None}),
3643        "new",
3644        1,
3645        Node::Heading(Heading{depth: 1, values: vec![
3646            Node::Text(Text{value: "first".to_string(), position: None}),
3647            Node::Text(Text{value: "new".to_string(), position: None})
3648        ], position: None}))]
3649    #[case(Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec![
3650        Node::Text(Text{value: "first".to_string(), position: None}),
3651        Node::Text(Text{value: "second".to_string(), position: None})
3652    ], position: None}),
3653        "new",
3654        0,
3655        Node::List(List{index: 0, level: 0, checked: None, ordered: false,  values: vec![
3656            Node::Text(Text{value: "new".to_string(), position: None}),
3657            Node::Text(Text{value: "second".to_string(), position: None})
3658        ], position: None}))]
3659    #[case(Node::TableCell(TableCell{column: 0, row: 0, values: vec![
3660        Node::Text(Text{value: "first".to_string(), position: None}),
3661        Node::Text(Text{value: "second".to_string(), position: None})
3662    ], position: None}),
3663        "new",
3664        1,
3665        Node::TableCell(TableCell{column: 0, row: 0, values: vec![
3666            Node::Text(Text{value: "first".to_string(), position: None}),
3667            Node::Text(Text{value: "new".to_string(), position: None})
3668        ], position: None}))]
3669    #[case(Node::Text(Text{value: "plain text".to_string(), position: None}),
3670        "new",
3671        0,
3672        Node::Text(Text{value: "plain text".to_string(), position: None}))]
3673    #[case(Node::Code(Code{value: "code".to_string(), lang: None, fence: true, meta: None, position: None}),
3674        "new",
3675        0,
3676        Node::Code(Code{value: "code".to_string(), lang: None, fence: true, meta: None, position: None}))]
3677    #[case(Node::List(List{index: 0, level: 1, checked: Some(true), ordered: false, values: vec![
3678        Node::Text(Text{value: "first".to_string(), position: None})
3679    ], position: None}),
3680        "new",
3681        0,
3682        Node::List(List{index: 0, level: 1, checked: Some(true), ordered: false, values: vec![
3683            Node::Text(Text{value: "new".to_string(), position: None})
3684        ], position: None}))]
3685    #[case(Node::List(List{index: 0, level: 1, checked: None, ordered: false, values: vec![
3686        Node::Text(Text{value: "first".to_string(), position: None})
3687    ], position: None}),
3688        "new",
3689        2,
3690        Node::List(List{index: 0, level: 1, checked: None, ordered: false, values: vec![
3691            Node::Text(Text{value: "first".to_string(), position: None})
3692        ], position: None}))]
3693    #[case::link_ref(Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![
3694            Node::Text(Text{value: "first".to_string(), position: None}),
3695            Node::Text(Text{value: "second".to_string(), position: None})
3696        ], label: None, position: None}), "new", 0, Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![
3697            Node::Text(Text{value: "new".to_string(), position: None}),
3698            Node::Text(Text{value: "second".to_string(), position: None})
3699        ], label: None, position: None}))]
3700    #[case(Node::MdxJsxFlowElement(MdxJsxFlowElement{
3701            name: Some("div".to_string()),
3702            attributes: Vec::new(),
3703            children: vec![
3704                Node::Text(Text{value: "first".to_string(), position: None}),
3705                Node::Text(Text{value: "second".to_string(), position: None})
3706            ],
3707            position: None
3708        }),
3709        "new",
3710        0,
3711        Node::MdxJsxFlowElement(MdxJsxFlowElement{
3712            name: Some("div".to_string()),
3713            attributes: Vec::new(),
3714            children: vec![
3715                Node::Text(Text{value: "new".to_string(), position: None}),
3716                Node::Text(Text{value: "second".to_string(), position: None})
3717            ],
3718            position: None
3719        }))]
3720    #[case(Node::MdxJsxFlowElement(MdxJsxFlowElement{
3721            name: Some("div".to_string()),
3722            attributes: Vec::new(),
3723            children: vec![
3724                Node::Text(Text{value: "first".to_string(), position: None}),
3725                Node::Text(Text{value: "second".to_string(), position: None})
3726            ],
3727            position: None
3728        }),
3729        "new",
3730        1,
3731        Node::MdxJsxFlowElement(MdxJsxFlowElement{
3732            name: Some("div".to_string()),
3733            attributes: Vec::new(),
3734            children: vec![
3735                Node::Text(Text{value: "first".to_string(), position: None}),
3736                Node::Text(Text{value: "new".to_string(), position: None})
3737            ],
3738            position: None
3739        }))]
3740    #[case(Node::MdxJsxTextElement(MdxJsxTextElement{
3741            name: Some("span".into()),
3742            attributes: Vec::new(),
3743            children: vec![
3744                Node::Text(Text{value: "first".to_string(), position: None}),
3745                Node::Text(Text{value: "second".to_string(), position: None})
3746            ],
3747            position: None
3748        }),
3749        "new",
3750        0,
3751        Node::MdxJsxTextElement(MdxJsxTextElement{
3752            name: Some("span".into()),
3753            attributes: Vec::new(),
3754            children: vec![
3755                Node::Text(Text{value: "new".to_string(), position: None}),
3756                Node::Text(Text{value: "second".to_string(), position: None})
3757            ],
3758            position: None
3759        }))]
3760    #[case(Node::MdxJsxTextElement(MdxJsxTextElement{
3761            name: Some("span".into()),
3762            attributes: Vec::new(),
3763            children: vec![
3764                Node::Text(Text{value: "first".to_string(), position: None}),
3765                Node::Text(Text{value: "second".to_string(), position: None})
3766            ],
3767            position: None
3768        }),
3769        "new",
3770        1,
3771        Node::MdxJsxTextElement(MdxJsxTextElement{
3772            name: Some("span".into()),
3773            attributes: Vec::new(),
3774            children: vec![
3775                Node::Text(Text{value: "first".to_string(), position: None}),
3776                Node::Text(Text{value: "new".to_string(), position: None})
3777            ],
3778            position: None
3779        }))]
3780    fn test_with_children_value(#[case] node: Node, #[case] value: &str, #[case] index: usize, #[case] expected: Node) {
3781        assert_eq!(node.with_children_value(value, index), expected);
3782    }
3783
3784    #[rstest]
3785    #[case(Node::Text(Text{value: "test".to_string(), position: None }),
3786           "test".to_string())]
3787    #[case(Node::List(List{index: 0, level: 2, checked: None, ordered: false, values: vec!["test".to_string().into()], position: None}),
3788           "    - test".to_string())]
3789    fn test_display(#[case] node: Node, #[case] expected: String) {
3790        assert_eq!(node.to_string_with(&RenderOptions::default()), expected);
3791    }
3792
3793    #[rstest]
3794    #[case(Node::Text(Text{value: "test".to_string(), position: None}), true)]
3795    #[case(Node::CodeInline(CodeInline{value: "test".into(), position: None}), false)]
3796    #[case(Node::MathInline(MathInline{value: "test".into(), position: None}), false)]
3797    fn test_is_text(#[case] node: Node, #[case] expected: bool) {
3798        assert_eq!(node.is_text(), expected);
3799    }
3800
3801    #[rstest]
3802    #[case(Node::CodeInline(CodeInline{value: "test".into(), position: None}), true)]
3803    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3804    fn test_is_inline_code(#[case] node: Node, #[case] expected: bool) {
3805        assert_eq!(node.is_inline_code(), expected);
3806    }
3807
3808    #[rstest]
3809    #[case(Node::MathInline(MathInline{value: "test".into(), position: None}), true)]
3810    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3811    fn test_is_inline_math(#[case] node: Node, #[case] expected: bool) {
3812        assert_eq!(node.is_inline_math(), expected);
3813    }
3814
3815    #[rstest]
3816    #[case(Node::Strong(Strong{values: vec!["test".to_string().into()], position: None}), true)]
3817    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3818    fn test_is_strong(#[case] node: Node, #[case] expected: bool) {
3819        assert_eq!(node.is_strong(), expected);
3820    }
3821
3822    #[rstest]
3823    #[case(Node::Delete(Delete{values: vec!["test".to_string().into()], position: None}), true)]
3824    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3825    fn test_is_delete(#[case] node: Node, #[case] expected: bool) {
3826        assert_eq!(node.is_delete(), expected);
3827    }
3828
3829    #[rstest]
3830    #[case(Node::Link(Link{url: Url::new("test".to_string()), values: Vec::new(), title: None, position: None}), true)]
3831    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3832    fn test_is_link(#[case] node: Node, #[case] expected: bool) {
3833        assert_eq!(node.is_link(), expected);
3834    }
3835
3836    #[rstest]
3837    #[case(Node::LinkRef(LinkRef{ident: "test".to_string(), values: Vec::new(), label: None, position: None}), true)]
3838    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3839    fn test_is_link_ref(#[case] node: Node, #[case] expected: bool) {
3840        assert_eq!(node.is_link_ref(), expected);
3841    }
3842
3843    #[rstest]
3844    #[case(Node::Image(Image{alt: attr_keys::ALT.to_string(), url: "test".to_string(), title: None, position: None}), true)]
3845    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3846    fn test_is_image(#[case] node: Node, #[case] expected: bool) {
3847        assert_eq!(node.is_image(), expected);
3848    }
3849
3850    #[rstest]
3851    #[case(Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "test".to_string(), label: None, position: None}), true)]
3852    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3853    fn test_is_image_ref(#[case] node: Node, #[case] expected: bool) {
3854        assert_eq!(node.is_image_ref(), expected);
3855    }
3856
3857    #[rstest]
3858    #[case(Node::Code(Code{value: "code".to_string(), lang: Some("rust".to_string()), fence: true, meta: None, position: None}), true, Some("rust".into()))]
3859    #[case(Node::Code(Code{value: "code".to_string(), lang: Some("rust".to_string()), fence: true, meta: None, position: None}), false, Some("python".into()))]
3860    #[case(Node::Code(Code{value: "code".to_string(), lang: None, fence: true, meta: None, position: None}), true, None)]
3861    #[case(Node::Code(Code{value: "code".to_string(), lang: None, fence: false, meta: None, position: None}), true, None)]
3862    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false, None)]
3863    fn test_is_code(#[case] node: Node, #[case] expected: bool, #[case] lang: Option<SmolStr>) {
3864        assert_eq!(node.is_code(lang), expected);
3865    }
3866
3867    #[rstest]
3868    #[case(Node::Heading(Heading{depth: 1, values: vec!["test".to_string().into()], position: None}), true, Some(1))]
3869    #[case(Node::Heading(Heading{depth: 2, values: vec!["test".to_string().into()], position: None}), false, Some(1))]
3870    #[case(Node::Heading(Heading{depth: 1, values: vec!["test".to_string().into()], position: None}), true, None)]
3871    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false, None)]
3872    fn test_is_heading(#[case] node: Node, #[case] expected: bool, #[case] depth: Option<u8>) {
3873        assert_eq!(node.is_heading(depth), expected);
3874    }
3875
3876    #[rstest]
3877    #[case(Node::HorizontalRule(HorizontalRule{position: None}), true)]
3878    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3879    fn test_is_horizontal_rule(#[case] node: Node, #[case] expected: bool) {
3880        assert_eq!(node.is_horizontal_rule(), expected);
3881    }
3882
3883    #[rstest]
3884    #[case(Node::Blockquote(Blockquote{values: vec!["test".to_string().into()], position: None}), true)]
3885    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3886    fn test_is_blockquote(#[case] node: Node, #[case] expected: bool) {
3887        assert_eq!(node.is_blockquote(), expected);
3888    }
3889
3890    #[cfg(feature = "wikilink")]
3891    #[rstest]
3892    #[case(Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}), true)]
3893    #[case(Node::WikiLink(WikiLink{target: "Three laws of motion".to_string(), text: Some("Newton".to_string()), position: None}), true)]
3894    #[case(Node::Link(Link{url: Url::new("https://example.com".to_string()), values: Vec::new(), title: None, position: None}), false)]
3895    #[case(Node::Link(Link{url: Url::new("relative.md".to_string()), values: Vec::new(), title: None, position: None}), false)]
3896    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3897    #[case(Node::Text(Text{value: "[[target]]".to_string(), position: None}), false)]
3898    fn test_is_wikilink(#[case] node: Node, #[case] expected: bool) {
3899        assert_eq!(node.is_wikilink(), expected);
3900    }
3901
3902    #[cfg(feature = "wikilink")]
3903    #[rstest]
3904    #[case(Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}), true)]
3905    #[case(Node::WikiLink(WikiLink{target: "Three laws of motion".to_string(), text: Some("Newton".to_string()), position: None}), true)]
3906    #[case(Node::Link(Link{url: Url::new("https://example.com".to_string()), values: Vec::new(), title: None, position: None}), true)]
3907    #[case(Node::Link(Link{url: Url::new("relative.md".to_string()), values: Vec::new(), title: None, position: None}), true)]
3908    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
3909    fn test_is_link_includes_wikilink(#[case] node: Node, #[case] expected: bool) {
3910        assert_eq!(node.is_link(), expected);
3911    }
3912
3913    #[cfg(feature = "wikilink")]
3914    #[rstest]
3915    // no wikilinks — returns original text node unchanged
3916    #[case("plain text", vec![Node::Text(Text{value: "plain text".to_string(), position: None})])]
3917    // only a wikilink
3918    #[case("[[target]]", vec![Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None})])]
3919    // wikilink with display text
3920    #[case("[[target|display]]", vec![Node::WikiLink(WikiLink{target: "target".to_string(), text: Some("display".to_string()), position: None})])]
3921    // wikilink with spaces in target
3922    #[case("[[Three laws of motion]]", vec![Node::WikiLink(WikiLink{target: "Three laws of motion".to_string(), text: None, position: None})])]
3923    // wikilink with .md extension
3924    #[case("[[target.md]]", vec![Node::WikiLink(WikiLink{target: "target.md".to_string(), text: None, position: None})])]
3925    // wikilink at start
3926    #[case("[[target]] after", vec![
3927        Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}),
3928        Node::Text(Text{value: " after".to_string(), position: None}),
3929    ])]
3930    // wikilink at end
3931    #[case("before [[target]]", vec![
3932        Node::Text(Text{value: "before ".to_string(), position: None}),
3933        Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}),
3934    ])]
3935    // wikilink in middle
3936    #[case("before [[target]] after", vec![
3937        Node::Text(Text{value: "before ".to_string(), position: None}),
3938        Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}),
3939        Node::Text(Text{value: " after".to_string(), position: None}),
3940    ])]
3941    // multiple wikilinks
3942    #[case("[[a]] and [[b]]", vec![
3943        Node::WikiLink(WikiLink{target: "a".to_string(), text: None, position: None}),
3944        Node::Text(Text{value: " and ".to_string(), position: None}),
3945        Node::WikiLink(WikiLink{target: "b".to_string(), text: None, position: None}),
3946    ])]
3947    // unclosed [[ treated as plain text
3948    #[case("[[unclosed", vec![Node::Text(Text{value: "[[unclosed".to_string(), position: None})])]
3949    // nested brackets invalid — treated as plain text
3950    #[case("[[in[ner]]]", vec![Node::Text(Text{value: "[[in[ner]]]".to_string(), position: None})])]
3951    // multibyte characters in surrounding text
3952    #[case("日本語 [[ターゲット]] テキスト", vec![
3953        Node::Text(Text{value: "日本語 ".to_string(), position: None}),
3954        Node::WikiLink(WikiLink{target: "ターゲット".to_string(), text: None, position: None}),
3955        Node::Text(Text{value: " テキスト".to_string(), position: None}),
3956    ])]
3957    // multibyte characters inside wikilink target and display text
3958    #[case("[[ページ|表示名]]", vec![
3959        Node::WikiLink(WikiLink{target: "ページ".to_string(), text: Some("表示名".to_string()), position: None}),
3960    ])]
3961    fn test_parse_wikilinks_in_text(#[case] input: &str, #[case] expected: Vec<Node>) {
3962        let result = Node::parse_wikilinks_in_text(input, None);
3963        assert_eq!(result, expected);
3964    }
3965
3966    #[cfg(feature = "wikilink")]
3967    #[rstest]
3968    #[case(Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}), "url", Some(AttrValue::String("target".to_string())))]
3969    #[case(Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}), "value", Some(AttrValue::String("target".to_string())))]
3970    #[case(Node::WikiLink(WikiLink{target: "target".to_string(), text: Some("display".to_string()), position: None}), "url", Some(AttrValue::String("target".to_string())))]
3971    #[case(Node::WikiLink(WikiLink{target: "target".to_string(), text: Some("display".to_string()), position: None}), "value", Some(AttrValue::String("display".to_string())))]
3972    #[case(Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}), "title", None)]
3973    fn test_wikilink_attr(#[case] node: Node, #[case] attr: &str, #[case] expected: Option<AttrValue>) {
3974        assert_eq!(node.attr(attr), expected);
3975    }
3976
3977    #[cfg(feature = "wikilink")]
3978    #[rstest]
3979    // no display text: with_value sets target
3980    #[case(
3981        Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}),
3982        "new-target",
3983        Node::WikiLink(WikiLink{target: "new-target".to_string(), text: None, position: None})
3984    )]
3985    // with display text: with_value sets text, not target
3986    #[case(
3987        Node::WikiLink(WikiLink{target: "page".to_string(), text: Some("Display Text".to_string()), position: None}),
3988        "DISPLAY TEXT",
3989        Node::WikiLink(WikiLink{target: "page".to_string(), text: Some("DISPLAY TEXT".to_string()), position: None})
3990    )]
3991    fn test_wikilink_with_value(#[case] node: Node, #[case] value: &str, #[case] expected: Node) {
3992        assert_eq!(node.with_value(value), expected);
3993    }
3994
3995    #[cfg(feature = "wikilink")]
3996    #[rstest]
3997    // footnote with no wikilinks — values unchanged
3998    #[case(
3999        Node::Footnote(Footnote{ident: "1".to_string(), values: vec![Node::Text(Text{value: "plain text".to_string(), position: None})], position: None}),
4000        vec![Node::Footnote(Footnote{ident: "1".to_string(), values: vec![Node::Text(Text{value: "plain text".to_string(), position: None})], position: None})]
4001    )]
4002    // footnote with a wikilink in text — expanded to WikiLink node
4003    #[case(
4004        Node::Footnote(Footnote{ident: "1".to_string(), values: vec![Node::Text(Text{value: "[[target]]".to_string(), position: None})], position: None}),
4005        vec![Node::Footnote(Footnote{ident: "1".to_string(), values: vec![Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None})], position: None})]
4006    )]
4007    // footnote with wikilink and display text
4008    #[case(
4009        Node::Footnote(Footnote{ident: "2".to_string(), values: vec![Node::Text(Text{value: "[[target|display]]".to_string(), position: None})], position: None}),
4010        vec![Node::Footnote(Footnote{ident: "2".to_string(), values: vec![Node::WikiLink(WikiLink{target: "target".to_string(), text: Some("display".to_string()), position: None})], position: None})]
4011    )]
4012    // footnote with mixed text and wikilink
4013    #[case(
4014        Node::Footnote(Footnote{ident: "3".to_string(), values: vec![Node::Text(Text{value: "see [[note]]".to_string(), position: None})], position: None}),
4015        vec![Node::Footnote(Footnote{ident: "3".to_string(), values: vec![
4016            Node::Text(Text{value: "see ".to_string(), position: None}),
4017            Node::WikiLink(WikiLink{target: "note".to_string(), text: None, position: None}),
4018        ], position: None})]
4019    )]
4020    fn test_expand_wikilinks_footnote(#[case] input: Node, #[case] expected: Vec<Node>) {
4021        let result = Node::expand_wikilinks(vec![input]);
4022        assert_eq!(result, expected);
4023    }
4024
4025    #[cfg(all(feature = "embed", feature = "wikilink"))]
4026    #[rstest]
4027    // embed only
4028    #[case("![[note.md]]", vec![
4029        Node::Embed(Embed{target: "note.md".to_string(), display: None, position: None}),
4030    ])]
4031    // wikilink only
4032    #[case("[[target]]", vec![
4033        Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}),
4034    ])]
4035    // embed and wikilink in same text
4036    #[case("![[embed]] and [[link]]", vec![
4037        Node::Embed(Embed{target: "embed".to_string(), display: None, position: None}),
4038        Node::Text(Text{value: " and ".to_string(), position: None}),
4039        Node::WikiLink(WikiLink{target: "link".to_string(), text: None, position: None}),
4040    ])]
4041    // wikilink before embed
4042    #[case("[[link]] and ![[embed]]", vec![
4043        Node::WikiLink(WikiLink{target: "link".to_string(), text: None, position: None}),
4044        Node::Text(Text{value: " and ".to_string(), position: None}),
4045        Node::Embed(Embed{target: "embed".to_string(), display: None, position: None}),
4046    ])]
4047    // embed with display hint
4048    #[case("![[image.png|400]]", vec![
4049        Node::Embed(Embed{target: "image.png".to_string(), display: Some("400".to_string()), position: None}),
4050    ])]
4051    // wikilink with display text
4052    #[case("[[page|display]]", vec![
4053        Node::WikiLink(WikiLink{target: "page".to_string(), text: Some("display".to_string()), position: None}),
4054    ])]
4055    // surrounding text
4056    #[case("before ![[note]] after [[link]] end", vec![
4057        Node::Text(Text{value: "before ".to_string(), position: None}),
4058        Node::Embed(Embed{target: "note".to_string(), display: None, position: None}),
4059        Node::Text(Text{value: " after ".to_string(), position: None}),
4060        Node::WikiLink(WikiLink{target: "link".to_string(), text: None, position: None}),
4061        Node::Text(Text{value: " end".to_string(), position: None}),
4062    ])]
4063    // plain text with no links
4064    #[case("just plain text", vec![
4065        Node::Text(Text{value: "just plain text".to_string(), position: None}),
4066    ])]
4067    // multibyte characters
4068    #[case("日本語 ![[ファイル]] と [[ページ]]", vec![
4069        Node::Text(Text{value: "日本語 ".to_string(), position: None}),
4070        Node::Embed(Embed{target: "ファイル".to_string(), display: None, position: None}),
4071        Node::Text(Text{value: " と ".to_string(), position: None}),
4072        Node::WikiLink(WikiLink{target: "ページ".to_string(), text: None, position: None}),
4073    ])]
4074    fn test_parse_inline_links_into(#[case] input: &str, #[case] expected: Vec<Node>) {
4075        let mut result = Vec::new();
4076        Node::parse_inline_links_into(input, None, &mut result);
4077        assert_eq!(result, expected);
4078    }
4079
4080    #[rstest]
4081    #[case(Node::Html(Html{value: "<div>test</div>".to_string(), position: None}), true)]
4082    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4083    fn test_is_html(#[case] node: Node, #[case] expected: bool) {
4084        assert_eq!(node.is_html(), expected);
4085    }
4086
4087    #[rstest]
4088    #[case(Node::node_values(
4089           &Node::Strong(Strong{values: vec!["test".to_string().into()], position: None})),
4090           vec!["test".to_string().into()])]
4091    #[case(Node::node_values(
4092           &Node::Text(Text{value: "test".to_string(), position: None})),
4093           vec!["test".to_string().into()])]
4094    #[case(Node::node_values(
4095           &Node::Blockquote(Blockquote{values: vec!["test".to_string().into()], position: None})),
4096           vec!["test".to_string().into()])]
4097    #[case(Node::node_values(
4098           &Node::Delete(Delete{values: vec!["test".to_string().into()], position: None})),
4099           vec!["test".to_string().into()])]
4100    #[case(Node::node_values(
4101           &Node::Emphasis(Emphasis{values: vec!["test".to_string().into()], position: None})),
4102           vec!["test".to_string().into()])]
4103    #[case(Node::node_values(
4104           &Node::Heading(Heading{depth: 1, values: vec!["test".to_string().into()], position: None})),
4105           vec!["test".to_string().into()])]
4106    #[case(Node::node_values(
4107           &Node::List(List{values: vec!["test".to_string().into()], ordered: false, level: 1, checked: Some(false), index: 0, position: None})),
4108           vec!["test".to_string().into()])]
4109    fn test_node_value(#[case] actual: Vec<Node>, #[case] expected: Vec<Node>) {
4110        assert_eq!(actual, expected);
4111    }
4112
4113    #[rstest]
4114    #[case(Node::Footnote(Footnote{ident: "test".to_string(), values: Vec::new(), position: None}), true)]
4115    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4116    fn test_is_footnote(#[case] node: Node, #[case] expected: bool) {
4117        assert_eq!(node.is_footnote(), expected);
4118    }
4119
4120    #[rstest]
4121    #[case(Node::FootnoteRef(FootnoteRef{ident: "test".to_string(), label: None, position: None}), true)]
4122    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4123    fn test_is_footnote_ref(#[case] node: Node, #[case] expected: bool) {
4124        assert_eq!(node.is_footnote_ref(), expected);
4125    }
4126
4127    #[rstest]
4128    #[case(Node::Math(Math{value: "x^2".to_string(), position: None}), true)]
4129    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4130    fn test_is_math(#[case] node: Node, #[case] expected: bool) {
4131        assert_eq!(node.is_math(), expected);
4132    }
4133
4134    #[rstest]
4135    #[case(Node::Break(Break{position: None}), true)]
4136    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4137    fn test_is_break(#[case] node: Node, #[case] expected: bool) {
4138        assert_eq!(node.is_break(), expected);
4139    }
4140
4141    #[rstest]
4142    #[case(Node::Yaml(Yaml{value: "key: value".to_string(), position: None}), true)]
4143    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4144    fn test_is_yaml(#[case] node: Node, #[case] expected: bool) {
4145        assert_eq!(node.is_yaml(), expected);
4146    }
4147
4148    #[rstest]
4149    #[case(Node::Toml(Toml{value: "key = \"value\"".to_string(), position: None}), true)]
4150    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4151    fn test_is_toml(#[case] node: Node, #[case] expected: bool) {
4152        assert_eq!(node.is_toml(), expected);
4153    }
4154
4155    #[rstest]
4156    #[case(Node::Definition(Definition{ident: attr_keys::IDENT.to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: None, position: None}), true)]
4157    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4158    fn test_is_definition(#[case] node: Node, #[case] expected: bool) {
4159        assert_eq!(node.is_definition(), expected);
4160    }
4161
4162    #[rstest]
4163    #[case(Node::Emphasis(Emphasis{values: vec!["test".to_string().into()], position: None}), true)]
4164    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4165    fn test_is_emphasis(#[case] node: Node, #[case] expected: bool) {
4166        assert_eq!(node.is_emphasis(), expected);
4167    }
4168
4169    #[rstest]
4170    #[case(Node::MdxFlowExpression(MdxFlowExpression{value: "test".into(), position: None}), true)]
4171    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4172    fn test_is_mdx_flow_expression(#[case] node: Node, #[case] expected: bool) {
4173        assert_eq!(node.is_mdx_flow_expression(), expected);
4174    }
4175
4176    #[rstest]
4177    #[case(Node::MdxTextExpression(MdxTextExpression{value: "test".into(), position: None}), true)]
4178    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4179    fn test_is_mdx_text_expression(#[case] node: Node, #[case] expected: bool) {
4180        assert_eq!(node.is_mdx_text_expression(), expected);
4181    }
4182
4183    #[rstest]
4184    #[case(Node::MdxJsxFlowElement(MdxJsxFlowElement{name: None, attributes: Vec::new(), children: Vec::new(), position: None}), true)]
4185    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4186    fn test_is_mdx_jsx_flow_element(#[case] node: Node, #[case] expected: bool) {
4187        assert_eq!(node.is_mdx_jsx_flow_element(), expected);
4188    }
4189
4190    #[rstest]
4191    #[case(Node::MdxJsxTextElement(MdxJsxTextElement{name: None, attributes: Vec::new(), children: Vec::new(), position: None}), true)]
4192    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4193    fn test_is_mdx_jsx_text_element(#[case] node: Node, #[case] expected: bool) {
4194        assert_eq!(node.is_mdx_jsx_text_element(), expected);
4195    }
4196
4197    #[rstest]
4198    #[case(Node::MdxJsEsm(MdxJsEsm{value: "test".into(), position: None}), true)]
4199    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4200    fn test_is_msx_js_esm(#[case] node: Node, #[case] expected: bool) {
4201        assert_eq!(node.is_mdx_js_esm(), expected);
4202    }
4203
4204    #[rstest]
4205    #[case::text(Node::Text(Text{value: "test".to_string(), position: None }), RenderOptions::default(), "test")]
4206    #[case::list(Node::List(List{index: 0, level: 2, checked: None, ordered: false, values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "    - test")]
4207    #[case::list(Node::List(List{index: 0, level: 1, checked: None, ordered: false, values: vec!["test".to_string().into()], position: None}), RenderOptions { list_style: ListStyle::Plus, ..Default::default() }, "  + test")]
4208    #[case::list(Node::List(List{index: 0, level: 1, checked: Some(true), ordered: false, values: vec!["test".to_string().into()], position: None}), RenderOptions { list_style: ListStyle::Star, ..Default::default() }, "  * [x] test")]
4209    #[case::list(Node::List(List{index: 0, level: 1, checked: Some(false), ordered: false, values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "  - [ ] test")]
4210    #[case::list(Node::List(List{index: 0, level: 1, checked: None, ordered: true, values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "  1. test")]
4211    #[case::list(Node::List(List{index: 0, level: 1, checked: Some(false), ordered: true, values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "  1. [ ] test")]
4212    #[case::table_row(Node::TableRow(TableRow{values: vec![Node::TableCell(TableCell{column: 0, row: 0, values: vec!["test".to_string().into()], position: None})], position: None}), RenderOptions::default(), "|test|")]
4213    #[case::table_row(Node::TableRow(TableRow{values: vec![Node::TableCell(TableCell{column: 0, row: 0, values: vec!["test".to_string().into()], position: None})], position: None}), RenderOptions::default(), "|test|")]
4214    #[case::table_cell(Node::TableCell(TableCell{column: 0, row: 0, values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "test")]
4215    #[case::table_cell(Node::TableCell(TableCell{column: 0, row: 0, values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "test")]
4216    #[case::table_align(Node::TableAlign(TableAlign{align: vec![TableAlignKind::Left, TableAlignKind::Right, TableAlignKind::Center, TableAlignKind::None], position: None}), RenderOptions::default(), "|:---|---:|:---:|---|")]
4217    #[case::block_quote(Node::Blockquote(Blockquote{values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "> test")]
4218    #[case::block_quote(Node::Blockquote(Blockquote{values: vec!["test\ntest2".to_string().into()], position: None}), RenderOptions::default(), "> test\n> test2")]
4219    #[case::code(Node::Code(Code{value: "code".to_string(), lang: Some("rust".to_string()), fence: true, meta: None, position: None}), RenderOptions::default(), "```rust\ncode\n```")]
4220    #[case::code(Node::Code(Code{value: "code".to_string(), lang: None, fence: true, meta: None, position: None}), RenderOptions::default(), "```\ncode\n```")]
4221    #[case::code(Node::Code(Code{value: "code".to_string(), lang: None, fence: false, meta: None, position: None}), RenderOptions::default(), "    code")]
4222    #[case::code(Node::Code(Code{value: "code".to_string(), lang: Some("rust".to_string()), fence: true, meta: Some("meta".to_string()), position: None}), RenderOptions::default(), "```rust meta\ncode\n```")]
4223    #[case::definition(Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: Some(attr_keys::LABEL.to_string()), position: None}), RenderOptions::default(), "[label]: url")]
4224    #[case::definition(Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: Some(Title::new(attr_keys::TITLE.to_string())), label: Some(attr_keys::LABEL.to_string()), position: None}), RenderOptions::default(), "[label]: url \"title\"")]
4225    #[case::definition(Node::Definition(Definition{ident: "id".to_string(), url: Url::new("".to_string()), title: None, label: Some(attr_keys::LABEL.to_string()), position: None}), RenderOptions::default(), "[label]: ")]
4226    #[case::delete(Node::Delete(Delete{values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "~~test~~")]
4227    #[case::emphasis(Node::Emphasis(Emphasis{values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "*test*")]
4228    #[case::footnote(Node::Footnote(Footnote{ident: "id".to_string(), values: vec![attr_keys::LABEL.to_string().into()], position: None}), RenderOptions::default(), "[^id]: label")]
4229    #[case::footnote_ref(Node::FootnoteRef(FootnoteRef{ident: attr_keys::LABEL.to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}), RenderOptions::default(), "[^label]")]
4230    #[case::heading(Node::Heading(Heading{depth: 1, values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "# test")]
4231    #[case::heading(Node::Heading(Heading{depth: 3, values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "### test")]
4232    #[case::html(Node::Html(Html{value: "<div>test</div>".to_string(), position: None}), RenderOptions::default(), "<div>test</div>")]
4233    #[case::image(Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: None, position: None}), RenderOptions::default(), "![alt](url)")]
4234    #[case::image(Node::Image(Image{alt: attr_keys::ALT.to_string(), url: "url with space".to_string(), title: Some(attr_keys::TITLE.to_string()), position: None}), RenderOptions::default(), "![alt](url%20with%20space \"title\")")]
4235    #[case::image_ref(Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: Some("id".to_string()), position: None}), RenderOptions::default(), "![alt][id]")]
4236    #[case::image_ref(Node::ImageRef(ImageRef{alt: "id".to_string(), ident: "id".to_string(), label: Some("id".to_string()), position: None}), RenderOptions::default(), "![id]")]
4237    #[case::code_inline(Node::CodeInline(CodeInline{value: "code".into(), position: None}), RenderOptions::default(), "`code`")]
4238    #[case::math_inline(Node::MathInline(MathInline{value: "x^2".into(), position: None}), RenderOptions::default(), "$x^2$")]
4239    #[case::link(Node::Link(Link{url: Url::new(attr_keys::URL.to_string()), title: Some(Title::new(attr_keys::TITLE.to_string())), values: vec![attr_keys::VALUE.to_string().into()], position: None}), RenderOptions::default(), "[value](url \"title\")")]
4240    #[case::link(Node::Link(Link{url: Url::new("".to_string()), title: None, values: vec![attr_keys::VALUE.to_string().into()], position: None}), RenderOptions::default(), "[value]()")]
4241    #[case::link(Node::Link(Link{url: Url::new(attr_keys::URL.to_string()), title: None, values: vec![attr_keys::VALUE.to_string().into()], position: None}), RenderOptions::default(), "[value](url)")]
4242    #[case::link_ref(Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec!["id".to_string().into()], label: Some("id".to_string()), position: None}), RenderOptions::default(), "[id]")]
4243    #[case::link_ref(Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec!["open".to_string().into()], label: Some("id".to_string()), position: None}), RenderOptions::default(), "[open][id]")]
4244    #[case::math(Node::Math(Math{value: "x^2".to_string(), position: None}), RenderOptions::default(), "$$\nx^2\n$$")]
4245    #[case::strong(Node::Strong(Strong{values: vec!["test".to_string().into()], position: None}), RenderOptions::default(), "**test**")]
4246    #[case::yaml(Node::Yaml(Yaml{value: "key: value".to_string(), position: None}), RenderOptions::default(), "---\nkey: value\n---")]
4247    #[case::toml(Node::Toml(Toml{value: "key = \"value\"".to_string(), position: None}), RenderOptions::default(), "+++\nkey = \"value\"\n+++")]
4248    #[case::break_(Node::Break(Break{position: None}), RenderOptions::default(), "\\")]
4249    #[case::horizontal_rule(Node::HorizontalRule(HorizontalRule{position: None}), RenderOptions::default(), "---")]
4250    #[case::mdx_jsx_flow_element(Node::MdxJsxFlowElement(MdxJsxFlowElement{
4251        name: Some("div".to_string()),
4252        attributes: vec![
4253            MdxAttributeContent::Property(MdxJsxAttribute {
4254                name: "className".into(),
4255                value: Some(MdxAttributeValue::Literal("container".into()))
4256            })
4257        ],
4258        children: vec![
4259            "content".to_string().into()
4260        ],
4261        position: None
4262    }), RenderOptions::default(), "<div className=\"container\">content</div>")]
4263    #[case::mdx_jsx_flow_element(Node::MdxJsxFlowElement(MdxJsxFlowElement{
4264        name: Some("div".to_string()),
4265        attributes: vec![
4266            MdxAttributeContent::Property(MdxJsxAttribute {
4267                name: "className".into(),
4268                value: Some(MdxAttributeValue::Literal("container".into()))
4269            })
4270        ],
4271        children: Vec::new(),
4272        position: None
4273    }), RenderOptions::default(), "<div className=\"container\" />")]
4274    #[case::mdx_jsx_flow_element(Node::MdxJsxFlowElement(MdxJsxFlowElement{
4275        name: Some("div".to_string()),
4276        attributes: Vec::new(),
4277        children: Vec::new(),
4278        position: None
4279    }), RenderOptions::default(), "<div />")]
4280    #[case::mdx_jsx_text_element(Node::MdxJsxTextElement(MdxJsxTextElement{
4281        name: Some("span".into()),
4282        attributes: vec![
4283            MdxAttributeContent::Expression("...props".into())
4284        ],
4285        children: vec![
4286            "inline".to_string().into()
4287        ],
4288        position: None
4289    }), RenderOptions::default(), "<span {...props}>inline</span>")]
4290    #[case::mdx_jsx_text_element(Node::MdxJsxTextElement(MdxJsxTextElement{
4291        name: Some("span".into()),
4292        attributes: vec![
4293            MdxAttributeContent::Expression("...props".into())
4294        ],
4295        children: vec![
4296        ],
4297        position: None
4298    }), RenderOptions::default(), "<span {...props} />")]
4299    #[case::mdx_jsx_text_element(Node::MdxJsxTextElement(MdxJsxTextElement{
4300        name: Some("span".into()),
4301        attributes: vec![
4302        ],
4303        children: vec![
4304        ],
4305        position: None
4306    }), RenderOptions::default(), "<span />")]
4307    #[case(Node::MdxTextExpression(MdxTextExpression{
4308        value: "count + 1".into(),
4309        position: None,
4310    }), RenderOptions::default(), "{count + 1}")]
4311    #[case(Node::MdxJsEsm(MdxJsEsm{
4312        value: "import React from 'react'".into(),
4313        position: None,
4314    }), RenderOptions::default(), "import React from 'react'")]
4315    #[case::fragment_empty(Node::Fragment(Fragment{values: vec![]}), RenderOptions::default(), "")]
4316    #[case::fragment_single(Node::Fragment(Fragment{values: vec![
4317        Node::Text(Text{value: "hello".to_string(), position: None})
4318    ]}), RenderOptions::default(), "hello")]
4319    #[case::fragment_multiple(Node::Fragment(Fragment{values: vec![
4320        Node::Text(Text{value: "hello".to_string(), position: None}),
4321        Node::Text(Text{value: "world".to_string(), position: None})
4322    ]}), RenderOptions::default(), "hello\nworld")]
4323    #[case::fragment_filters_empty(Node::Fragment(Fragment{values: vec![
4324        Node::Text(Text{value: "hello".to_string(), position: None}),
4325        Node::Empty,
4326        Node::Text(Text{value: "world".to_string(), position: None})
4327    ]}), RenderOptions::default(), "hello\nworld")]
4328    #[case::fragment_all_empty(Node::Fragment(Fragment{values: vec![
4329        Node::Empty,
4330        Node::Empty,
4331    ]}), RenderOptions::default(), "")]
4332    #[cfg_attr(feature = "wikilink", case::wikilink(Node::WikiLink(WikiLink{target: "target".to_string(), text: None, position: None}), RenderOptions::default(), "[[target]]"))]
4333    #[cfg_attr(feature = "wikilink", case::wikilink_with_text(Node::WikiLink(WikiLink{target: "target".to_string(), text: Some("display text".to_string()), position: None}), RenderOptions::default(), "[[target|display text]]"))]
4334    #[cfg_attr(feature = "wikilink", case::wikilink_same_text(Node::WikiLink(WikiLink{target: "target".to_string(), text: Some("target".to_string()), position: None}), RenderOptions::default(), "[[target]]"))]
4335    fn test_to_string_with(#[case] node: Node, #[case] options: RenderOptions, #[case] expected: &str) {
4336        assert_eq!(node.to_string_with(&options), expected);
4337    }
4338
4339    #[test]
4340    fn test_node_partial_ord() {
4341        let node1 = Node::Text(Text {
4342            value: "test1".to_string(),
4343            position: Some(Position {
4344                start: Point { line: 1, column: 1 },
4345                end: Point { line: 1, column: 5 },
4346            }),
4347        });
4348
4349        let node2 = Node::Text(Text {
4350            value: "test2".to_string(),
4351            position: Some(Position {
4352                start: Point { line: 1, column: 6 },
4353                end: Point { line: 1, column: 10 },
4354            }),
4355        });
4356
4357        let node3 = Node::Text(Text {
4358            value: "test3".to_string(),
4359            position: Some(Position {
4360                start: Point { line: 2, column: 1 },
4361                end: Point { line: 2, column: 5 },
4362            }),
4363        });
4364
4365        assert_eq!(node1.partial_cmp(&node2), Some(std::cmp::Ordering::Less));
4366        assert_eq!(node2.partial_cmp(&node1), Some(std::cmp::Ordering::Greater));
4367
4368        assert_eq!(node1.partial_cmp(&node3), Some(std::cmp::Ordering::Less));
4369        assert_eq!(node3.partial_cmp(&node1), Some(std::cmp::Ordering::Greater));
4370
4371        let node4 = Node::Text(Text {
4372            value: "test4".to_string(),
4373            position: None,
4374        });
4375
4376        assert_eq!(node1.partial_cmp(&node4), Some(std::cmp::Ordering::Less));
4377        assert_eq!(node4.partial_cmp(&node1), Some(std::cmp::Ordering::Greater));
4378
4379        let node5 = Node::Text(Text {
4380            value: "test5".to_string(),
4381            position: None,
4382        });
4383
4384        assert_eq!(node4.partial_cmp(&node5), Some(std::cmp::Ordering::Equal));
4385
4386        let node6 = Node::Code(Code {
4387            value: "code".to_string(),
4388            lang: None,
4389            fence: true,
4390            meta: None,
4391            position: None,
4392        });
4393
4394        assert_eq!(node6.partial_cmp(&node4), Some(std::cmp::Ordering::Less));
4395        assert_eq!(node4.partial_cmp(&node6), Some(std::cmp::Ordering::Greater));
4396    }
4397
4398    #[rstest]
4399    #[case(Node::Blockquote(Blockquote{values: Vec::new(), position: None}), "blockquote")]
4400    #[case(Node::Break(Break{position: None}), "break")]
4401    #[case(Node::Definition(Definition{ident: "".to_string(), url: Url::new("".to_string()), title: None, label: None, position: None}), "definition")]
4402    #[case(Node::Delete(Delete{values: Vec::new(), position: None}), "delete")]
4403    #[case(Node::Heading(Heading{depth: 1, values: Vec::new(), position: None}), "h1")]
4404    #[case(Node::Heading(Heading{depth: 2, values: Vec::new(), position: None}), "h2")]
4405    #[case(Node::Heading(Heading{depth: 3, values: Vec::new(), position: None}), "h3")]
4406    #[case(Node::Heading(Heading{depth: 4, values: Vec::new(), position: None}), "h4")]
4407    #[case(Node::Heading(Heading{depth: 5, values: Vec::new(), position: None}), "h5")]
4408    #[case(Node::Heading(Heading{depth: 6, values: Vec::new(), position: None}), "h6")]
4409    #[case(Node::Heading(Heading{depth: 7, values: Vec::new(), position: None}), "h")]
4410    #[case(Node::Emphasis(Emphasis{values: Vec::new(), position: None}), "emphasis")]
4411    #[case(Node::Footnote(Footnote{ident: "".to_string(), values: Vec::new(), position: None}), "footnote")]
4412    #[case(Node::FootnoteRef(FootnoteRef{ident: "".to_string(), label: None, position: None}), "footnoteref")]
4413    #[case(Node::Html(Html{value: "".to_string(), position: None}), "html")]
4414    #[case(Node::Yaml(Yaml{value: "".to_string(), position: None}), "yaml")]
4415    #[case(Node::Toml(Toml{value: "".to_string(), position: None}), "toml")]
4416    #[case(Node::Image(Image{alt: "".to_string(), url: "".to_string(), title: None, position: None}), "image")]
4417    #[case(Node::ImageRef(ImageRef{alt: "".to_string(), ident: "".to_string(), label: None, position: None}), "image_ref")]
4418    #[case(Node::CodeInline(CodeInline{value: "".into(), position: None}), "code_inline")]
4419    #[case(Node::MathInline(MathInline{value: "".into(), position: None}), "math_inline")]
4420    #[case(Node::Link(Link{url: Url::new("".to_string()), title: None, values: Vec::new(), position: None}), "link")]
4421    #[case(Node::LinkRef(LinkRef{ident: "".to_string(), values: Vec::new(), label: None, position: None}), "link_ref")]
4422    #[case(Node::Math(Math{value: "".to_string(), position: None}), "math")]
4423    #[case(Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: Vec::new(), position: None}), "list")]
4424    #[case(Node::TableAlign(TableAlign{align: Vec::new(), position: None}), "table_align")]
4425    #[case(Node::TableRow(TableRow{values: Vec::new(), position: None}), "table_row")]
4426    #[case(Node::TableCell(TableCell{column: 0, row: 0, values: Vec::new(), position: None}), "table_cell")]
4427    #[case(Node::Code(Code{value: "".to_string(), lang: None, fence: true, meta: None, position: None}), "code")]
4428    #[case(Node::Strong(Strong{values: Vec::new(), position: None}), "strong")]
4429    #[case(Node::HorizontalRule(HorizontalRule{position: None}), "Horizontal_rule")]
4430    #[case(Node::MdxFlowExpression(MdxFlowExpression{value: "".into(), position: None}), "mdx_flow_expression")]
4431    #[case(Node::MdxJsxFlowElement(MdxJsxFlowElement{name: None, attributes: Vec::new(), children: Vec::new(), position: None}), "mdx_jsx_flow_element")]
4432    #[case(Node::MdxJsxTextElement(MdxJsxTextElement{name: None, attributes: Vec::new(), children: Vec::new(), position: None}), "mdx_jsx_text_element")]
4433    #[case(Node::MdxTextExpression(MdxTextExpression{value: "".into(), position: None}), "mdx_text_expression")]
4434    #[case(Node::MdxJsEsm(MdxJsEsm{value: "".into(), position: None}), "mdx_js_esm")]
4435    #[case(Node::Text(Text{value: "".to_string(), position: None}), "text")]
4436    fn test_name(#[case] node: Node, #[case] expected: &str) {
4437        assert_eq!(node.name(), expected);
4438    }
4439
4440    #[rstest]
4441    #[case(Node::Text(Text{value: "test".to_string(), position: None}), "test")]
4442    #[case(Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}), "test")]
4443    #[case(Node::Blockquote(Blockquote{values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}), "test")]
4444    #[case(Node::Delete(Delete{values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}), "test")]
4445    #[case(Node::Heading(Heading{depth: 1, values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}), "test")]
4446    #[case(Node::Emphasis(Emphasis{values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}), "test")]
4447    #[case(Node::Footnote(Footnote{ident: "test".to_string(), values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}), "test")]
4448    #[case(Node::FootnoteRef(FootnoteRef{ident: "test".to_string(), label: None, position: None}), "test")]
4449    #[case(Node::Html(Html{value: "test".to_string(), position: None}), "test")]
4450    #[case(Node::Yaml(Yaml{value: "test".to_string(), position: None}), "test")]
4451    #[case(Node::Toml(Toml{value: "test".to_string(), position: None}), "test")]
4452    #[case(Node::Image(Image{alt: attr_keys::ALT.to_string(), url: "test".to_string(), title: None, position: None}), "test")]
4453    #[case(Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "test".to_string(), label: None, position: None}), "test")]
4454    #[case(Node::CodeInline(CodeInline{value: "test".into(), position: None}), "test")]
4455    #[case(Node::MathInline(MathInline{value: "test".into(), position: None}), "test")]
4456    #[case(Node::Link(Link{url: Url::new("test".to_string()), title: None, values: Vec::new(), position: None}), "test")]
4457    #[case(Node::LinkRef(LinkRef{ident: "test".to_string(), values: Vec::new(), label: None, position: None}), "test")]
4458    #[case(Node::Math(Math{value: "test".to_string(), position: None}), "test")]
4459    #[case(Node::Code(Code{value: "test".to_string(), lang: None, fence: true, meta: None, position: None}), "test")]
4460    #[case(Node::Strong(Strong{values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}), "test")]
4461    #[case(Node::TableCell(TableCell{column: 0, row: 0, values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}), "test")]
4462    #[case(Node::TableRow(TableRow{values: vec![Node::TableCell(TableCell{column: 0, row: 0, values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None})], position: None}), "test")]
4463    #[case(Node::Break(Break{position: None}), "")]
4464    #[case(Node::HorizontalRule(HorizontalRule{position: None}), "")]
4465    #[case(Node::TableAlign(TableAlign{align: Vec::new(), position: None}), "")]
4466    #[case(Node::MdxFlowExpression(MdxFlowExpression{value: "test".into(), position: None}), "test")]
4467    #[case(Node::MdxTextExpression(MdxTextExpression{value: "test".into(), position: None}), "test")]
4468    #[case(Node::MdxJsEsm(MdxJsEsm{value: "test".into(), position: None}), "test")]
4469    #[case(Node::MdxJsxFlowElement(MdxJsxFlowElement{name: Some(attr_keys::NAME.to_string()), attributes: Vec::new(), children: vec![Node::Text(Text{value: "test".to_string(), position: None})],  position: None}), "test")]
4470    #[case(Node::Definition(Definition{ident: "test".to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: None, position: None}), attr_keys::URL)]
4471    #[case(Node::Fragment(Fragment {values: vec![Node::Text(Text{value: "test".to_string(), position: None})]}), "test")]
4472    fn test_value(#[case] node: Node, #[case] expected: &str) {
4473        assert_eq!(node.value(), expected);
4474    }
4475
4476    #[rstest]
4477    #[case(Node::Text(Text{value: "test".to_string(), position: None}), None)]
4478    #[case(Node::Text(Text{value: "test".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4479    #[case(Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4480    #[case(Node::Blockquote(Blockquote{values: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4481    #[case(Node::Delete(Delete{values: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4482    #[case(Node::Heading(Heading{depth: 1, values: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4483    #[case(Node::Emphasis(Emphasis{values: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4484    #[case(Node::Footnote(Footnote{ident: "".to_string(), values: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4485    #[case(Node::FootnoteRef(FootnoteRef{ident: "".to_string(), label: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4486    #[case(Node::Html(Html{value: "".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4487    #[case(Node::Yaml(Yaml{value: "".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4488    #[case(Node::Toml(Toml{value: "".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4489    #[case(Node::Image(Image{alt: "".to_string(), url: "".to_string(), title: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4490    #[case(Node::ImageRef(ImageRef{alt: "".to_string(), ident: "".to_string(), label: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4491    #[case(Node::CodeInline(CodeInline{value: "".into(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4492    #[case(Node::MathInline(MathInline{value: "".into(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4493    #[case(Node::Link(Link{url: Url("".to_string()), title: None, values: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4494    #[case(Node::LinkRef(LinkRef{ident: "".to_string(), values: Vec::new(), label: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4495    #[case(Node::Math(Math{value: "".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4496    #[case(Node::Code(Code{value: "".to_string(), lang: None, fence: true, meta: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4497    #[case(Node::Strong(Strong{values: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4498    #[case(Node::TableCell(TableCell{column: 0, row: 0, values: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4499    #[case(Node::TableRow(TableRow{values: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4500    #[case(Node::TableAlign(TableAlign{align: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4501    #[case(Node::Break(Break{position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4502    #[case(Node::HorizontalRule(HorizontalRule{position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4503    #[case(Node::MdxFlowExpression(MdxFlowExpression{value: "test".into(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4504    #[case(Node::MdxTextExpression(MdxTextExpression{value: "test".into(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4505    #[case(Node::MdxJsEsm(MdxJsEsm{value: "test".into(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4506    #[case(Node::MdxJsxFlowElement(MdxJsxFlowElement{name: Some("div".to_string()), attributes: Vec::new(), children: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4507    #[case(Node::MdxJsxTextElement(MdxJsxTextElement{name: Some("span".into()), attributes: Vec::new(), children: Vec::new(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4508    #[case(Node::Definition(Definition{ident: "".to_string(), url: Url("".to_string()), title: None, label: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4509    #[case(Node::Fragment(Fragment{values: Vec::new()}), None)]
4510    #[case(Node::Fragment(Fragment{values: vec![
4511        Node::Text(Text{value: "test1".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}),
4512        Node::Text(Text{value: "test2".to_string(), position: Some(Position{start: Point{line: 1, column: 6}, end: Point{line: 1, column: 10}})})
4513    ]}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}}))]
4514    #[case(Node::Fragment(Fragment{values: vec![
4515        Node::Text(Text{value: "test".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}),
4516        Node::Text(Text{value: "test2".to_string(), position: None})
4517    ]}), Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}}))]
4518    #[case(Node::Fragment(Fragment{values: vec![
4519        Node::Text(Text{value: "test".to_string(), position: None}),
4520        Node::Text(Text{value: "test2".to_string(), position: Some(Position{start: Point{line: 1, column: 6}, end: Point{line: 1, column: 10}})})
4521    ]}), Some(Position{start: Point{line: 1, column: 6}, end: Point{line: 1, column: 10}}))]
4522    #[case(Node::Fragment(Fragment{values: vec![
4523        Node::Text(Text{value: "test2".to_string(), position: Some(Position{start: Point{line: 1, column: 6}, end: Point{line: 1, column: 10}})}),
4524        Node::Text(Text{value: "test".to_string(), position: None})
4525    ]}), Some(Position{start: Point{line: 1, column: 6}, end: Point{line: 1, column: 10}}))]
4526    #[case(Node::Fragment(Fragment{values: vec![
4527        Node::Text(Text{value: "test".to_string(), position: None}),
4528        Node::Text(Text{value: "test2".to_string(), position: None})
4529    ]}), None)]
4530    #[case(Node::Empty, None)]
4531    fn test_position(#[case] node: Node, #[case] expected: Option<Position>) {
4532        assert_eq!(node.position(), expected);
4533    }
4534
4535    #[rstest]
4536    #[case(Node::Blockquote(Blockquote{values: vec![
4537        Node::Text(Text{value: "first".to_string(), position: None}),
4538        Node::Text(Text{value: "second".to_string(), position: None})
4539    ], position: None}), 0, Some(Node::Text(Text{value: "first".to_string(), position: None})))]
4540    #[case(Node::Blockquote(Blockquote{values: vec![
4541        Node::Text(Text{value: "first".to_string(), position: None}),
4542        Node::Text(Text{value: "second".to_string(), position: None})
4543    ], position: None}), 1, Some(Node::Text(Text{value: "second".to_string(), position: None})))]
4544    #[case(Node::Blockquote(Blockquote{values: vec![
4545        Node::Text(Text{value: "first".to_string(), position: None})
4546    ], position: None}), 1, None)]
4547    #[case(Node::Delete(Delete{values: vec![
4548        Node::Text(Text{value: "first".to_string(), position: None}),
4549        Node::Text(Text{value: "second".to_string(), position: None})
4550    ], position: None}), 0, Some(Node::Text(Text{value: "first".to_string(), position: None})))]
4551    #[case(Node::Emphasis(Emphasis{values: vec![
4552        Node::Text(Text{value: "first".to_string(), position: None}),
4553        Node::Text(Text{value: "second".to_string(), position: None})
4554    ], position: None}), 1, Some(Node::Text(Text{value: "second".to_string(), position: None})))]
4555    #[case(Node::Strong(Strong{values: vec![
4556        Node::Text(Text{value: "first".to_string(), position: None})
4557    ], position: None}), 0, Some(Node::Text(Text{value: "first".to_string(), position: None})))]
4558    #[case(Node::Heading(Heading{depth: 1, values: vec![
4559        Node::Text(Text{value: "first".to_string(), position: None}),
4560        Node::Text(Text{value: "second".to_string(), position: None})
4561    ], position: None}), 0, Some(Node::Text(Text{value: "first".to_string(), position: None})))]
4562    #[case(Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec![
4563        Node::Text(Text{value: "first".to_string(), position: None}),
4564        Node::Text(Text{value: "second".to_string(), position: None})
4565    ], position: None}), 1, Some(Node::Text(Text{value: "second".to_string(), position: None})))]
4566    #[case(Node::TableCell(TableCell{column: 0, row: 0, values: vec![
4567        Node::Text(Text{value: "cell content".to_string(), position: None})
4568    ], position: None}), 0, Some(Node::Text(Text{value: "cell content".to_string(), position: None})))]
4569    #[case(Node::TableRow(TableRow{values: vec![
4570        Node::TableCell(TableCell{column: 0, row: 0, values: Vec::new(), position: None}),
4571        Node::TableCell(TableCell{column: 1, row: 0, values: Vec::new(), position: None})
4572    ], position: None}), 1, Some(Node::TableCell(TableCell{column: 1, row: 0, values: Vec::new(), position: None})))]
4573    #[case(Node::Text(Text{value: "plain text".to_string(), position: None}), 0, None)]
4574    #[case(Node::Code(Code{value: "code".to_string(), lang: None, fence: true, meta: None, position: None}), 0, None)]
4575    #[case(Node::Html(Html{value: "<div>".to_string(), position: None}), 0, None)]
4576    fn test_find_at_index(#[case] node: Node, #[case] index: usize, #[case] expected: Option<Node>) {
4577        assert_eq!(node.find_at_index(index), expected);
4578    }
4579
4580    #[rstest]
4581    #[case(Node::Blockquote(Blockquote{values: vec!["test".to_string().into()], position: None}),
4582           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4583    #[case(Node::Delete(Delete{values: vec!["test".to_string().into()], position: None}),
4584           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4585    #[case(Node::Heading(Heading{depth: 1, values: vec!["test".to_string().into()], position: None}),
4586           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4587    #[case(Node::Emphasis(Emphasis{values: vec!["test".to_string().into()], position: None}),
4588           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4589    #[case(Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec!["test".to_string().into()], position: None}),
4590           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4591    #[case(Node::Strong(Strong{values: vec!["test".to_string().into()], position: None}),
4592           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4593    #[case(Node::Link(Link{url: Url(attr_keys::URL.to_string()), title: None, values: vec!["test".to_string().into()], position: None}),
4594           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4595    #[case(Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec!["test".to_string().into()], label: None, position: None}),
4596           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4597    #[case(Node::Footnote(Footnote{ident: "id".to_string(), values: vec!["test".to_string().into()], position: None}),
4598           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4599    #[case(Node::TableCell(TableCell{column: 0, row: 0, values: vec!["test".to_string().into()], position: None}),
4600           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4601    #[case(Node::TableRow(TableRow{values: vec!["test".to_string().into()], position: None}),
4602           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4603    #[case(Node::Fragment(Fragment{values: vec!["test".to_string().into()]}),
4604           Node::Fragment(Fragment{values: vec!["test".to_string().into()]}))]
4605    #[case(Node::Text(Text{value: "test".to_string(), position: None}),
4606           Node::Empty)]
4607    #[case(Node::Code(Code{value: "test".to_string(), lang: None, fence: true, meta: None, position: None}),
4608           Node::Empty)]
4609    #[case(Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: None, position: None}),
4610           Node::Empty)]
4611    #[case(Node::Empty, Node::Empty)]
4612    fn test_to_fragment(#[case] node: Node, #[case] expected: Node) {
4613        assert_eq!(node.to_fragment(), expected);
4614    }
4615
4616    #[rstest]
4617    #[case(
4618        &mut Node::Blockquote(Blockquote{values: vec![
4619            Node::Text(Text{value: "old".to_string(), position: None})
4620        ], position: None}),
4621        Node::Fragment(Fragment{values: vec![
4622            Node::Text(Text{value: "new".to_string(), position: None})
4623        ]}),
4624        Node::Blockquote(Blockquote{values: vec![
4625            Node::Text(Text{value: "new".to_string(), position: None})
4626        ], position: None})
4627    )]
4628    #[case(
4629        &mut Node::Delete(Delete{values: vec![
4630            Node::Text(Text{value: "old".to_string(), position: None})
4631        ], position: None}),
4632        Node::Fragment(Fragment{values: vec![
4633            Node::Text(Text{value: "new".to_string(), position: None})
4634        ]}),
4635        Node::Delete(Delete{values: vec![
4636            Node::Text(Text{value: "new".to_string(), position: None})
4637        ], position: None})
4638    )]
4639    #[case(
4640        &mut Node::Emphasis(Emphasis{values: vec![
4641            Node::Text(Text{value: "old".to_string(), position: None})
4642        ], position: None}),
4643        Node::Fragment(Fragment{values: vec![
4644            Node::Text(Text{value: "new".to_string(), position: None})
4645        ]}),
4646        Node::Emphasis(Emphasis{values: vec![
4647            Node::Text(Text{value: "new".to_string(), position: None})
4648        ], position: None})
4649    )]
4650    #[case(
4651        &mut Node::Strong(Strong{values: vec![
4652            Node::Text(Text{value: "old".to_string(), position: None})
4653        ], position: None}),
4654        Node::Fragment(Fragment{values: vec![
4655            Node::Text(Text{value: "new".to_string(), position: None})
4656        ]}),
4657        Node::Strong(Strong{values: vec![
4658            Node::Text(Text{value: "new".to_string(), position: None})
4659        ], position: None})
4660    )]
4661    #[case(
4662        &mut Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec![
4663            Node::Text(Text{value: "old".to_string(), position: None})
4664        ], position: None}),
4665        Node::Fragment(Fragment{values: vec![
4666            Node::Text(Text{value: "new".to_string(), position: None})
4667        ]}),
4668        Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec![
4669            Node::Text(Text{value: "new".to_string(), position: None})
4670        ], position: None})
4671    )]
4672    #[case(
4673        &mut Node::Heading(Heading{depth: 1, values: vec![
4674            Node::Text(Text{value: "old".to_string(), position: None})
4675        ], position: None}),
4676        Node::Fragment(Fragment{values: vec![
4677            Node::Text(Text{value: "new".to_string(), position: None})
4678        ]}),
4679        Node::Heading(Heading{depth: 1, values: vec![
4680            Node::Text(Text{value: "new".to_string(), position: None})
4681        ], position: None})
4682    )]
4683    #[case(
4684        &mut Node::Link(Link{url: Url(attr_keys::URL.to_string()), title: None, values: vec![
4685            Node::Text(Text{value: "old".to_string(), position: None})
4686        ], position: None}),
4687        Node::Fragment(Fragment{values: vec![
4688            Node::Text(Text{value: "new".to_string(), position: None})
4689        ]}),
4690        Node::Link(Link{url: Url(attr_keys::URL.to_string()), title: None, values: vec![
4691            Node::Text(Text{value: "new".to_string(), position: None})
4692        ], position: None})
4693    )]
4694    #[case(
4695        &mut Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![
4696            Node::Text(Text{value: "old".to_string(), position: None})
4697        ], label: None, position: None}),
4698        Node::Fragment(Fragment{values: vec![
4699            Node::Text(Text{value: "new".to_string(), position: None})
4700        ]}),
4701        Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![
4702            Node::Text(Text{value: "new".to_string(), position: None})
4703        ], label: None, position: None})
4704    )]
4705    #[case(
4706        &mut Node::Footnote(Footnote{ident: "id".to_string(), values: vec![
4707            Node::Text(Text{value: "old".to_string(), position: None})
4708        ], position: None}),
4709        Node::Fragment(Fragment{values: vec![
4710            Node::Text(Text{value: "new".to_string(), position: None})
4711        ]}),
4712        Node::Footnote(Footnote{ident: "id".to_string(), values: vec![
4713            Node::Text(Text{value: "new".to_string(), position: None})
4714        ], position: None})
4715    )]
4716    #[case(
4717        &mut Node::TableCell(TableCell{column: 0, row: 0, values: vec![
4718            Node::Text(Text{value: "old".to_string(), position: None})
4719        ], position: None}),
4720        Node::Fragment(Fragment{values: vec![
4721            Node::Text(Text{value: "new".to_string(), position: None})
4722        ]}),
4723        Node::TableCell(TableCell{column: 0, row: 0, values: vec![
4724            Node::Text(Text{value: "new".to_string(), position: None})
4725        ], position: None})
4726    )]
4727    #[case(
4728        &mut Node::TableRow(TableRow{values: vec![
4729            Node::TableCell(TableCell{column: 0, row: 0, values: vec![
4730                Node::Text(Text{value: "old".to_string(), position: None})
4731            ], position: None})
4732        ], position: None}),
4733        Node::Fragment(Fragment{values: vec![
4734            Node::TableCell(TableCell{column: 0, row: 0, values: vec![
4735                Node::Text(Text{value: "new".to_string(), position: None})
4736            ], position: None})
4737        ]}),
4738        Node::TableRow(TableRow{values: vec![
4739            Node::TableCell(TableCell{column: 0, row: 0, values: vec![
4740                Node::Text(Text{value: "new".to_string(), position: None})
4741            ], position: None})
4742        ], position: None})
4743    )]
4744    #[case(
4745        &mut Node::Text(Text{value: "old".to_string(), position: None}),
4746        Node::Fragment(Fragment{values: vec![
4747            Node::Text(Text{value: "new".to_string(), position: None})
4748        ]}),
4749        Node::Text(Text{value: "old".to_string(), position: None})
4750    )]
4751    #[case(
4752        &mut Node::Blockquote(Blockquote{values: vec![
4753            Node::Text(Text{value: "text1".to_string(), position: None}),
4754            Node::Text(Text{value: "text2".to_string(), position: None})
4755        ], position: None}),
4756        Node::Fragment(Fragment{values: vec![
4757            Node::Text(Text{value: "new1".to_string(), position: None}),
4758            Node::Text(Text{value: "new2".to_string(), position: None})
4759        ]}),
4760        Node::Blockquote(Blockquote{values: vec![
4761            Node::Text(Text{value: "new1".to_string(), position: None}),
4762            Node::Text(Text{value: "new2".to_string(), position: None})
4763        ], position: None})
4764    )]
4765    #[case(
4766        &mut Node::Strong(Strong{values: vec![
4767            Node::Text(Text{value: "text1".to_string(), position: None}),
4768            Node::Text(Text{value: "text2".to_string(), position: None})
4769        ], position: None}),
4770        Node::Fragment(Fragment{values: vec![
4771            Node::Empty,
4772            Node::Text(Text{value: "new2".to_string(), position: None})
4773        ]}),
4774        Node::Strong(Strong{values: vec![
4775            Node::Text(Text{value: "text1".to_string(), position: None}),
4776            Node::Text(Text{value: "new2".to_string(), position: None})
4777        ], position: None})
4778    )]
4779    #[case(
4780        &mut Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec![
4781            Node::Text(Text{value: "text1".to_string(), position: None}),
4782            Node::Text(Text{value: "text2".to_string(), position: None})
4783        ], position: None}),
4784        Node::Fragment(Fragment{values: vec![
4785            Node::Text(Text{value: "new1".to_string(), position: None}),
4786            Node::Fragment(Fragment{values: Vec::new()})
4787        ]}),
4788        Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec![
4789            Node::Text(Text{value: "new1".to_string(), position: None}),
4790            Node::Text(Text{value: "text2".to_string(), position: None})
4791        ], position: None})
4792    )]
4793    fn test_apply_fragment(#[case] node: &mut Node, #[case] fragment: Node, #[case] expected: Node) {
4794        node.apply_fragment(fragment);
4795        assert_eq!(*node, expected);
4796    }
4797
4798    #[rstest]
4799    #[case(Node::Text(Text{value: "test".to_string(), position: None}),
4800       Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4801       Node::Text(Text{value: "test".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}))]
4802    #[case(Node::Code(Code{value: "code".to_string(), lang: None, fence: true, meta: None, position: None}),
4803       Position{start: Point{line: 1, column: 1}, end: Point{line: 3, column: 3}},
4804       Node::Code(Code{value: "code".to_string(), lang: None, fence: true, meta: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 3, column: 3}})}))]
4805    #[case(Node::List(List{index: 0, level: 1, checked: None, ordered: false, values: vec![], position: None}),
4806       Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4807       Node::List(List{index: 0, level: 1, checked: None, ordered: false, values: vec![], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}))]
4808    #[case(Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: None, position: None}),
4809       Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}},
4810       Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})}))]
4811    #[case(Node::Delete(Delete{values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}),
4812        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4813        Node::Delete(Delete{values: vec![Node::Text(Text{value: "test".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})})], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}))]
4814    #[case(Node::Emphasis(Emphasis{values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}),
4815        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4816        Node::Emphasis(Emphasis{values: vec![Node::Text(Text{value: "test".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})})], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}))]
4817    #[case(Node::Footnote(Footnote{ident: "id".to_string(), values: vec![Node::Text(Text{value: "test".to_string(), position: None})], position: None}),
4818        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4819        Node::Footnote(Footnote{ident: "id".to_string(), values: vec![Node::Text(Text{value: "test".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})})], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}))]
4820    #[case(Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}),
4821        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4822        Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}))]
4823    #[case(Node::Html(Html{value: "<div>test</div>".to_string(), position: None}),
4824        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 15}},
4825        Node::Html(Html{value: "<div>test</div>".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 15}})}))]
4826    #[case(Node::Yaml(Yaml{value: "key: value".to_string(), position: None}),
4827        Position{start: Point{line: 1, column: 1}, end: Point{line: 3, column: 4}},
4828        Node::Yaml(Yaml{value: "key: value".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 3, column: 4}})}))]
4829    #[case(Node::Toml(Toml{value: "key = \"value\"".to_string(), position: None}),
4830        Position{start: Point{line: 1, column: 1}, end: Point{line: 3, column: 4}},
4831        Node::Toml(Toml{value: "key = \"value\"".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 3, column: 4}})}))]
4832    #[case(Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: None, position: None}),
4833        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 12}},
4834        Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 12}})}))]
4835    #[case(Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: None, position: None}),
4836        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}},
4837        Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})}))]
4838    #[case(Node::CodeInline(CodeInline{value: "code".into(), position: None}),
4839        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 7}},
4840        Node::CodeInline(CodeInline{value: "code".into(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 7}})}))]
4841    #[case(Node::MathInline(MathInline{value: "x^2".into(), position: None}),
4842        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4843        Node::MathInline(MathInline{value: "x^2".into(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}))]
4844    #[case(Node::Link(Link{url: Url::new(attr_keys::URL.to_string()), title: None, values: vec![Node::Text(Text{value: "text".to_string(), position: None})], position: None}),
4845        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}},
4846        Node::Link(Link{url: Url::new(attr_keys::URL.to_string()), title: None, values: vec![Node::Text(Text{value: "text".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})})], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})}))]
4847    #[case(Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![Node::Text(Text{value: "text".to_string(), position: None})], label: None, position: None}),
4848        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}},
4849        Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![Node::Text(Text{value: "text".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})})], label: None, position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})}))]
4850    #[case(Node::Math(Math{value: "x^2".to_string(), position: None}),
4851        Position{start: Point{line: 1, column: 1}, end: Point{line: 3, column: 3}},
4852        Node::Math(Math{value: "x^2".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 3, column: 3}})}))]
4853    #[case(Node::TableCell(TableCell{column: 0, row: 0, values: vec![Node::Text(Text{value: "cell".to_string(), position: None})], position: None}),
4854        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 6}},
4855        Node::TableCell(TableCell{column: 0, row: 0, values: vec![Node::Text(Text{value: "cell".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 6}})})], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 6}})}))]
4856    #[case(Node::TableAlign(TableAlign{align: vec![TableAlignKind::Left, TableAlignKind::Right], position: None}),
4857        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 15}},
4858        Node::TableAlign(TableAlign{align: vec![TableAlignKind::Left, TableAlignKind::Right], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 15}})}))]
4859    #[case(Node::MdxFlowExpression(MdxFlowExpression{value: "test".into(), position: None}),
4860        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 7}},
4861        Node::MdxFlowExpression(MdxFlowExpression{value: "test".into(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 7}})}))]
4862    #[case(Node::MdxTextExpression(MdxTextExpression{value: "test".into(), position: None}),
4863        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 7}},
4864        Node::MdxTextExpression(MdxTextExpression{value: "test".into(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 7}})}))]
4865    #[case(Node::MdxJsEsm(MdxJsEsm{value: "import React from 'react'".into(), position: None}),
4866        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 25}},
4867        Node::MdxJsEsm(MdxJsEsm{value: "import React from 'react'".into(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 25}})}))]
4868    #[case(Node::MdxJsxTextElement(MdxJsxTextElement{name: Some("span".into()), attributes: Vec::new(), children: vec![Node::Text(Text{value: "text".to_string(), position: None})], position: None}),
4869        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 20}},
4870        Node::MdxJsxTextElement(MdxJsxTextElement{name: Some("span".into()), attributes: Vec::new(), children: vec![Node::Text(Text{value: "text".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 20}})})], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 20}})}))]
4871    #[case(Node::Break(Break{position: None}),
4872        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 2}},
4873        Node::Break(Break{position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 2}})}))]
4874    #[case(Node::Empty,
4875       Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4876       Node::Empty)]
4877    #[case(Node::Fragment(Fragment{values: vec![
4878           Node::Text(Text{value: "test1".to_string(), position: None}),
4879           Node::Text(Text{value: "test2".to_string(), position: None})
4880       ]}),
4881       Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}},
4882       Node::Fragment(Fragment{values: vec![
4883           Node::Text(Text{value: "test1".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})}),
4884           Node::Text(Text{value: "test2".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})})
4885       ]}))]
4886    #[case(Node::Blockquote(Blockquote{values: vec![
4887        Node::Text(Text{value: "test".to_string(), position: None})], position: None}),
4888        Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4889        Node::Blockquote(Blockquote{values: vec![
4890            Node::Text(Text{value: "test".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})})
4891        ], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}))]
4892    #[case(Node::Heading(Heading{depth: 1, values: vec![
4893            Node::Text(Text{value: "test".to_string(), position: None})], position: None}),
4894            Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4895            Node::Heading(Heading{depth: 1, values: vec![
4896                Node::Text(Text{value: "test".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})})
4897            ], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}))]
4898    #[case(Node::Strong(Strong{values: vec![
4899            Node::Text(Text{value: "test".to_string(), position: None})], position: None}),
4900            Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}},
4901            Node::Strong(Strong{values: vec![
4902                Node::Text(Text{value: "test".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})})
4903            ], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 5}})}))]
4904    #[case(Node::TableRow(TableRow{values: vec![
4905            Node::TableCell(TableCell{column: 0, row: 0, values: vec![
4906                Node::Text(Text{value: "cell".to_string(), position: None})
4907            ], position: None})
4908        ], position: None}),
4909            Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}},
4910            Node::TableRow(TableRow{values: vec![
4911                Node::TableCell(TableCell{column: 0, row: 0, values: vec![
4912                    Node::Text(Text{value: "cell".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})})
4913                ], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})})
4914            ], position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 10}})}))]
4915    #[case(Node::MdxJsxFlowElement(MdxJsxFlowElement{
4916            name: Some("div".to_string()),
4917            attributes: Vec::new(),
4918            children: vec![Node::Text(Text{value: "content".to_string(), position: None})],
4919            position: None
4920        }),
4921            Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 20}},
4922            Node::MdxJsxFlowElement(MdxJsxFlowElement{
4923                name: Some("div".to_string()),
4924                attributes: Vec::new(),
4925                children: vec![Node::Text(Text{value: "content".to_string(), position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 20}})})],
4926                position: Some(Position{start: Point{line: 1, column: 1}, end: Point{line: 1, column: 20}})
4927            }))]
4928    fn test_set_position(#[case] mut node: Node, #[case] position: Position, #[case] expected: Node) {
4929        node.set_position(Some(position));
4930        assert_eq!(node, expected);
4931    }
4932
4933    #[rstest]
4934    #[case(Node::List(List{index: 0, level: 0, checked: None, ordered: false, values: vec!["test".to_string().into()], position: None}), true)]
4935    #[case(Node::List(List{index: 1, level: 2, checked: Some(true), ordered: false, values: vec!["test".to_string().into()], position: None}), true)]
4936    #[case(Node::Text(Text{value: "test".to_string(), position: None}), false)]
4937    fn test_is_list(#[case] node: Node, #[case] expected: bool) {
4938        assert_eq!(node.is_list(), expected);
4939    }
4940
4941    #[rstest]
4942    #[case(Url::new("https://example.com".to_string()), RenderOptions{link_url_style: UrlSurroundStyle::None, ..Default::default()}, "https://example.com")]
4943    #[case(Url::new("https://example.com".to_string()), RenderOptions{link_url_style: UrlSurroundStyle::Angle, ..Default::default()}, "<https://example.com>")]
4944    #[case(Url::new("".to_string()), RenderOptions::default(), "")]
4945    fn test_url_to_string_with(#[case] url: Url, #[case] options: RenderOptions, #[case] expected: &str) {
4946        assert_eq!(url.to_string_with(&options), expected);
4947    }
4948
4949    #[rstest]
4950    #[case(Title::new(attr_keys::TITLE.to_string()), RenderOptions::default(), "\"title\"")]
4951    #[case(Title::new(r#"title with "quotes""#.to_string()), RenderOptions::default(), r#""title with "quotes"""#)]
4952    #[case(Title::new("title with spaces".to_string()), RenderOptions::default(), "\"title with spaces\"")]
4953    #[case(Title::new("".to_string()), RenderOptions::default(), "\"\"")]
4954    #[case(Title::new(attr_keys::TITLE.to_string()), RenderOptions{link_title_style: TitleSurroundStyle::Single, ..Default::default()}, "'title'")]
4955    #[case(Title::new("title with 'quotes'".to_string()), RenderOptions{link_title_style: TitleSurroundStyle::Double, ..Default::default()}, "\"title with 'quotes'\"")]
4956    #[case(Title::new(attr_keys::TITLE.to_string()), RenderOptions{link_title_style: TitleSurroundStyle::Paren, ..Default::default()}, "(title)")]
4957    fn test_title_to_string_with(#[case] title: Title, #[case] options: RenderOptions, #[case] expected: &str) {
4958        assert_eq!(title.to_string_with(&options), expected);
4959    }
4960
4961    #[rstest]
4962    #[case(Node::Fragment(Fragment{values: vec![]}), true)]
4963    #[case(Node::Fragment(Fragment{values: vec![
4964        Node::Text(Text{value: "not_empty".to_string(), position: None})
4965    ]}), false)]
4966    #[case(Node::Fragment(Fragment{values: vec![
4967        Node::Fragment(Fragment{values: vec![]}),
4968        Node::Fragment(Fragment{values: vec![]})
4969    ]}), true)]
4970    #[case(Node::Fragment(Fragment{values: vec![
4971        Node::Fragment(Fragment{values: vec![]}),
4972        Node::Text(Text{value: "not_empty".to_string(), position: None})
4973    ]}), false)]
4974    #[case(Node::Text(Text{value: "not_fragment".to_string(), position: None}), false)]
4975    fn test_is_empty_fragment(#[case] node: Node, #[case] expected: bool) {
4976        assert_eq!(node.is_empty_fragment(), expected);
4977    }
4978
4979    #[rstest]
4980    #[case::footnote(Node::Footnote(Footnote{ident: "id".to_string(), values: Vec::new(), position: None}), attr_keys::IDENT, Some(AttrValue::String("id".to_string())))]
4981    #[case::footnote(Node::Footnote(Footnote{ident: "id".to_string(), values: Vec::new(), position: None}), "unknown", None)]
4982    #[case::html(Node::Html(Html{value: "<div>test</div>".to_string(), position: None}), attr_keys::VALUE, Some(AttrValue::String("<div>test</div>".to_string())))]
4983    #[case::html(Node::Html(Html{value: "<div>test</div>".to_string(), position: None}), "unknown", None)]
4984    #[case::text(Node::Text(Text{value: "text".to_string(), position: None}), attr_keys::VALUE, Some(AttrValue::String("text".to_string())))]
4985    #[case::text(Node::Text(Text{value: "text".to_string(), position: None}), "unknown", None)]
4986    #[case::code(Node::Code(Code{value: "code".to_string(), lang: Some("rust".to_string()), meta: Some("meta".to_string()), fence: true, position: None}), attr_keys::VALUE, Some(AttrValue::String("code".to_string())))]
4987    #[case::code(Node::Code(Code{value: "code".to_string(), lang: Some("rust".to_string()), meta: Some("meta".to_string()), fence: true, position: None}), attr_keys::LANG, Some(AttrValue::String("rust".to_string())))]
4988    #[case::code(Node::Code(Code{value: "code".to_string(), lang: Some("rust".to_string()), meta: Some("meta".to_string()), fence: true, position: None}), "meta", Some(AttrValue::String("meta".to_string())))]
4989    #[case::code(Node::Code(Code{value: "code".to_string(), lang: Some("rust".to_string()), meta: Some("meta".to_string()), fence: true, position: None}), attr_keys::FENCE, Some(AttrValue::Boolean(true)))]
4990    #[case::code(Node::Code(Code{value: "code".to_string(), lang: None, meta: None, fence: false, position: None}), attr_keys::FENCE, Some(AttrValue::Boolean(false)))]
4991    #[case::code_inline(Node::CodeInline(CodeInline{value: "inline".into(), position: None}), attr_keys::VALUE, Some(AttrValue::String("inline".to_string())))]
4992    #[case::math_inline(Node::MathInline(MathInline{value: "math".into(), position: None}), attr_keys::VALUE, Some(AttrValue::String("math".to_string())))]
4993    #[case::math(Node::Math(Math{value: "math".to_string(), position: None}), attr_keys::VALUE, Some(AttrValue::String("math".to_string())))]
4994    #[case::yaml(Node::Yaml(Yaml{value: "yaml".to_string(), position: None}), attr_keys::VALUE, Some(AttrValue::String("yaml".to_string())))]
4995    #[case::toml(Node::Toml(Toml{value: "toml".to_string(), position: None}), attr_keys::VALUE, Some(AttrValue::String("toml".to_string())))]
4996    #[case::image(Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: Some(attr_keys::TITLE.to_string()), position: None}), attr_keys::ALT, Some(AttrValue::String(attr_keys::ALT.to_string())))]
4997    #[case::image(Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: Some(attr_keys::TITLE.to_string()), position: None}), attr_keys::URL, Some(AttrValue::String(attr_keys::URL.to_string())))]
4998    #[case::image(Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: Some(attr_keys::TITLE.to_string()), position: None}), attr_keys::TITLE, Some(AttrValue::String(attr_keys::TITLE.to_string())))]
4999    #[case::image_ref(Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::ALT, Some(AttrValue::String(attr_keys::ALT.to_string())))]
5000    #[case::image_ref(Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::IDENT, Some(AttrValue::String("id".to_string())))]
5001    #[case::image_ref(Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::LABEL, Some(AttrValue::String(attr_keys::LABEL.to_string())))]
5002    #[case::link(Node::Link(Link{url: Url::new(attr_keys::URL.to_string()), title: Some(Title::new(attr_keys::TITLE.to_string())), values: Vec::new(), position: None}), attr_keys::URL, Some(AttrValue::String(attr_keys::URL.to_string())))]
5003    #[case::link(Node::Link(Link{url: Url::new(attr_keys::URL.to_string()), title: Some(Title::new(attr_keys::TITLE.to_string())), values: Vec::new(), position: None}), attr_keys::TITLE, Some(AttrValue::String(attr_keys::TITLE.to_string())))]
5004    #[case::link_ref(Node::LinkRef(LinkRef{ident: "id".to_string(), values: Vec::new(), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::IDENT, Some(AttrValue::String("id".to_string())))]
5005    #[case::link_ref(Node::LinkRef(LinkRef{ident: "id".to_string(), values: Vec::new(), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::LABEL, Some(AttrValue::String(attr_keys::LABEL.to_string())))]
5006    #[case::footnote_ref(Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::IDENT, Some(AttrValue::String("id".to_string())))]
5007    #[case::footnote_ref(Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::LABEL, Some(AttrValue::String(attr_keys::LABEL.to_string())))]
5008    #[case::definition(Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: Some(Title::new(attr_keys::TITLE.to_string())), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::IDENT, Some(AttrValue::String("id".to_string())))]
5009    #[case::definition(Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: Some(Title::new(attr_keys::TITLE.to_string())), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::URL, Some(AttrValue::String(attr_keys::URL.to_string())))]
5010    #[case::definition(Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: Some(Title::new(attr_keys::TITLE.to_string())), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::TITLE, Some(AttrValue::String(attr_keys::TITLE.to_string())))]
5011    #[case::definition(Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: Some(Title::new(attr_keys::TITLE.to_string())), label: Some(attr_keys::LABEL.to_string()), position: None}), attr_keys::LABEL, Some(AttrValue::String(attr_keys::LABEL.to_string())))]
5012    #[case::heading(Node::Heading(Heading{depth: 3, values: Vec::new(), position: None}), "depth", Some(AttrValue::Integer(3)))]
5013    #[case::list(Node::List(List{index: 2, level: 1, checked: Some(true), ordered: true, values: Vec::new(), position: None}), "index", Some(AttrValue::Integer(2)))]
5014    #[case::list(Node::List(List{index: 2, level: 1, checked: Some(true), ordered: true, values: Vec::new(), position: None}), "level", Some(AttrValue::Integer(1)))]
5015    #[case::list(Node::List(List{index: 2, level: 1, checked: Some(true), ordered: true, values: Vec::new(), position: None}), "ordered", Some(AttrValue::Boolean(true)))]
5016    #[case::list(Node::List(List{index: 2, level: 1, checked: Some(true), ordered: true, values: Vec::new(), position: None}), attr_keys::CHECKED, Some(AttrValue::Boolean(true)))]
5017    #[case::table_cell(Node::TableCell(TableCell{column: 1, row: 2, values: Vec::new(), position: None}), "column", Some(AttrValue::Integer(1)))]
5018    #[case::table_cell(Node::TableCell(TableCell{column: 1, row: 2, values: Vec::new(), position: None}), "row", Some(AttrValue::Integer(2)))]
5019    #[case::table_align(Node::TableAlign(TableAlign{align: vec![TableAlignKind::Left, TableAlignKind::Right], position: None}), "align", Some(AttrValue::String(":---,---:".to_string())))]
5020    #[case::mdx_flow_expression(Node::MdxFlowExpression(MdxFlowExpression{value: "expr".into(), position: None}), attr_keys::VALUE, Some(AttrValue::String("expr".to_string())))]
5021    #[case::mdx_flow_expression(Node::MdxTextExpression(MdxTextExpression{value: "expr".into(), position: None}), attr_keys::VALUE, Some(AttrValue::String("expr".to_string())))]
5022    #[case::mdx_js_esm(Node::MdxJsEsm(MdxJsEsm{value: "esm".into(), position: None}), attr_keys::VALUE, Some(AttrValue::String("esm".to_string())))]
5023    #[case::mdx_jsx_flow_element(Node::MdxJsxFlowElement(MdxJsxFlowElement{name: Some("div".to_string()), attributes: Vec::new(), children: Vec::new(), position: None}), attr_keys::NAME, Some(AttrValue::String("div".to_string())))]
5024    #[case::mdx_jsx_flow_element(Node::MdxJsxTextElement(MdxJsxTextElement{name: Some("span".into()), attributes: Vec::new(), children: Vec::new(), position: None}), attr_keys::NAME, Some(AttrValue::String("span".to_string())))]
5025    #[case::break_(Node::Break(Break{position: None}), attr_keys::VALUE, None)]
5026    #[case::horizontal_rule(Node::HorizontalRule(HorizontalRule{position: None}), attr_keys::VALUE, None)]
5027    #[case::fragment(Node::Fragment(Fragment{values: Vec::new()}), attr_keys::VALUE, Some(AttrValue::String("".to_string())))]
5028    #[case::heading(Node::Heading(Heading{depth: 1, values: vec![Node::Text(Text{value: "heading text".to_string(), position: None})], position: None}), attr_keys::VALUE, Some(AttrValue::String("heading text".to_string())))]
5029    #[case::heading(Node::Heading(Heading{depth: 2, values: vec![], position: None}), attr_keys::VALUE, Some(AttrValue::String("".to_string())))]
5030    #[case::heading(Node::Heading(Heading{depth: 3, values: vec![
5031        Node::Text(Text{value: "first".to_string(), position: None}),
5032        Node::Text(Text{value: "second".to_string(), position: None}),
5033    ], position: None}), attr_keys::VALUE, Some(AttrValue::String("firstsecond".to_string())))]
5034    #[case(
5035        Node::List(List {
5036            index: 0,
5037            level: 1,
5038            checked: None,
5039            ordered: false,
5040            values: vec![
5041                Node::Text(Text { value: "item1".to_string(), position: None }),
5042                Node::Text(Text { value: "item2".to_string(), position: None }),
5043            ],
5044            position: None,
5045        }),
5046        attr_keys::VALUE,
5047        Some(AttrValue::String("item1item2".to_string()))
5048    )]
5049    #[case(
5050        Node::TableCell(TableCell {
5051            column: 1,
5052            row: 2,
5053            values: vec![Node::Text(Text {
5054                value: "cell_value".to_string(),
5055                position: None,
5056            })],
5057            position: None,
5058        }),
5059        attr_keys::VALUE,
5060        Some(AttrValue::String("cell_value".to_string()))
5061    )]
5062    #[case::footnote(
5063        Node::Footnote(Footnote {
5064            ident: "id".to_string(),
5065            values: vec![Node::Text(Text {
5066                value: "footnote value".to_string(),
5067                position: None,
5068            })],
5069            position: None,
5070        }),
5071        attr_keys::VALUE,
5072        Some(AttrValue::String("footnote value".to_string()))
5073    )]
5074    #[case::link(
5075        Node::Link(Link {
5076            url: Url::new("https://example.com".to_string()),
5077            title: Some(Title::new("Example".to_string())),
5078            values: vec![Node::Text(Text {
5079                value: "link text".to_string(),
5080                position: None,
5081            })],
5082            position: None,
5083        }),
5084        attr_keys::VALUE,
5085        Some(AttrValue::String("link text".to_string()))
5086    )]
5087    #[case::empty(Node::Empty, attr_keys::VALUE, None)]
5088    #[case::heading(
5089        Node::Heading(Heading {
5090            depth: 1,
5091            values: vec![
5092            Node::Text(Text {
5093                value: "child1".to_string(),
5094                position: None,
5095            }),
5096            Node::Text(Text {
5097                value: "child2".to_string(),
5098                position: None,
5099            }),
5100            ],
5101            position: None,
5102        }),
5103        attr_keys::CHILDREN,
5104        Some(AttrValue::Array(vec![
5105            Node::Text(Text {
5106            value: "child1".to_string(),
5107            position: None,
5108            }),
5109            Node::Text(Text {
5110            value: "child2".to_string(),
5111            position: None,
5112            }),
5113        ]))
5114        )]
5115    #[case::list(
5116        Node::List(List {
5117            index: 0,
5118            level: 1,
5119            checked: None,
5120            ordered: false,
5121            values: vec![
5122            Node::Text(Text {
5123                value: "item1".to_string(),
5124                position: None,
5125            }),
5126            ],
5127            position: None,
5128        }),
5129        attr_keys::CHILDREN,
5130        Some(AttrValue::Array(vec![
5131            Node::Text(Text {
5132            value: "item1".to_string(),
5133            position: None,
5134            }),
5135        ]))
5136        )]
5137    #[case::blockquote(
5138        Node::Blockquote(Blockquote {
5139            values: vec![
5140            Node::Text(Text {
5141                value: "quote".to_string(),
5142                position: None,
5143            }),
5144            ],
5145            position: None,
5146        }),
5147        attr_keys::VALUES,
5148        Some(AttrValue::Array(vec![
5149            Node::Text(Text {
5150            value: "quote".to_string(),
5151            position: None,
5152            }),
5153        ]))
5154        )]
5155    #[case::link(
5156        Node::Link(Link {
5157            url: Url::new(attr_keys::URL.to_string()),
5158            title: None,
5159            values: vec![
5160            Node::Text(Text {
5161                value: "link".to_string(),
5162                position: None,
5163            }),
5164            ],
5165            position: None,
5166        }),
5167        attr_keys::VALUES,
5168        Some(AttrValue::Array(vec![
5169            Node::Text(Text {
5170            value: "link".to_string(),
5171            position: None,
5172            }),
5173        ]))
5174        )]
5175    #[case::table_cell(
5176        Node::TableCell(TableCell {
5177            column: 0,
5178            row: 0,
5179            values: vec![
5180            Node::Text(Text {
5181                value: "cell".to_string(),
5182                position: None,
5183            }),
5184            ],
5185            position: None,
5186        }),
5187        attr_keys::CHILDREN,
5188        Some(AttrValue::Array(vec![
5189            Node::Text(Text {
5190            value: "cell".to_string(),
5191            position: None,
5192            }),
5193        ]))
5194        )]
5195    #[case::strong(
5196        Node::Strong(Strong {
5197            values: vec![
5198            Node::Text(Text {
5199                value: "bold".to_string(),
5200                position: None,
5201            }),
5202            ],
5203            position: None,
5204        }),
5205        attr_keys::CHILDREN,
5206        Some(AttrValue::Array(vec![
5207            Node::Text(Text {
5208            value: "bold".to_string(),
5209            position: None,
5210            }),
5211        ]))
5212        )]
5213    #[case::em(
5214        Node::Emphasis(Emphasis {
5215            values: vec![],
5216            position: None,
5217        }),
5218        attr_keys::CHILDREN,
5219        Some(AttrValue::Array(vec![]))
5220        )]
5221    fn test_attr(#[case] node: Node, #[case] attr: &str, #[case] expected: Option<AttrValue>) {
5222        assert_eq!(node.attr(attr), expected);
5223    }
5224
5225    #[rstest]
5226    #[case(
5227        Node::Text(Text{value: "old".to_string(), position: None}),
5228        attr_keys::VALUE,
5229        "new",
5230        Node::Text(Text{value: "new".to_string(), position: None})
5231    )]
5232    #[case(
5233        Node::Code(Code{value: "old".to_string(), lang: Some("rust".to_string()), fence: true, meta: None, position: None}),
5234        attr_keys::VALUE,
5235        "new_code",
5236        Node::Code(Code{value: "new_code".to_string(), lang: Some("rust".to_string()), fence: true, meta: None, position: None})
5237    )]
5238    #[case(
5239        Node::Code(Code{value: "code".to_string(), lang: Some("rust".to_string()), fence: true, meta: None, position: None}),
5240        attr_keys::LANG,
5241        "python",
5242        Node::Code(Code{value: "code".to_string(), lang: Some("python".to_string()), fence: true, meta: None, position: None})
5243    )]
5244    #[case(
5245        Node::Code(Code{value: "code".to_string(), lang: None, fence: false, meta: None, position: None}),
5246        attr_keys::FENCE,
5247        "true",
5248        Node::Code(Code{value: "code".to_string(), lang: None, fence: true, meta: None, position: None})
5249    )]
5250    #[case(
5251        Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: None, position: None}),
5252        attr_keys::ALT,
5253        "new_alt",
5254        Node::Image(Image{alt: "new_alt".to_string(), url: attr_keys::URL.to_string(), title: None, position: None})
5255    )]
5256    #[case(
5257        Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: None, position: None}),
5258        attr_keys::URL,
5259        "new_url",
5260        Node::Image(Image{alt: attr_keys::ALT.to_string(), url: "new_url".to_string(), title: None, position: None})
5261    )]
5262    #[case(
5263        Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: Some(attr_keys::TITLE.to_string()), position: None}),
5264        attr_keys::TITLE,
5265        "new_title",
5266        Node::Image(Image{alt: attr_keys::ALT.to_string(), url: attr_keys::URL.to_string(), title: Some("new_title".to_string()), position: None})
5267    )]
5268    #[case(
5269        Node::Heading(Heading{depth: 2, values: vec![], position: None}),
5270        "depth",
5271        "3",
5272        Node::Heading(Heading{depth: 3, values: vec![], position: None})
5273    )]
5274    #[case(
5275        Node::List(List{index: 1, level: 2, checked: Some(true), ordered: false, values: vec![], position: None}),
5276        attr_keys::CHECKED,
5277        "false",
5278        Node::List(List{index: 1, level: 2, checked: Some(false), ordered: false, values: vec![], position: None})
5279    )]
5280    #[case(
5281        Node::List(List{index: 1, level: 2, checked: Some(true), ordered: false, values: vec![], position: None}),
5282        "ordered",
5283        "true",
5284        Node::List(List{index: 1, level: 2, checked: Some(true), ordered: true, values: vec![], position: None})
5285    )]
5286    #[case(
5287        Node::TableCell(TableCell{column: 1, row: 2, values: vec![], position: None}),
5288        "column",
5289        "3",
5290        Node::TableCell(TableCell{column: 3, row: 2, values: vec![], position: None})
5291    )]
5292    #[case(
5293        Node::TableCell(TableCell{column: 1, row: 2, values: vec![], position: None}),
5294        "row",
5295        "5",
5296        Node::TableCell(TableCell{column: 1, row: 5, values: vec![], position: None})
5297    )]
5298    #[case(
5299        Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: None, position: None}),
5300        attr_keys::IDENT,
5301        "new_id",
5302        Node::Definition(Definition{ident: "new_id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: None, position: None})
5303    )]
5304    #[case(
5305        Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: None, position: None}),
5306        attr_keys::URL,
5307        "new_url",
5308        Node::Definition(Definition{ident: "id".to_string(), url: Url::new("new_url".to_string()), title: None, label: None, position: None})
5309    )]
5310    #[case(
5311        Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: None, position: None}),
5312        attr_keys::LABEL,
5313        "new_label",
5314        Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: Some("new_label".to_string()), position: None})
5315    )]
5316    #[case(
5317        Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: None, label: None, position: None}),
5318        attr_keys::TITLE,
5319        "new_title",
5320        Node::Definition(Definition{ident: "id".to_string(), url: Url::new(attr_keys::URL.to_string()), title: Some(Title::new("new_title".to_string())), label: None, position: None})
5321    )]
5322    #[case(
5323        Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}),
5324        attr_keys::ALT,
5325        "new_alt",
5326        Node::ImageRef(ImageRef{alt: "new_alt".to_string(), ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None})
5327    )]
5328    #[case(
5329        Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}),
5330        attr_keys::IDENT,
5331        "new_id",
5332        Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "new_id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None})
5333    )]
5334    #[case(
5335        Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}),
5336        attr_keys::LABEL,
5337        "new_label",
5338        Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: Some("new_label".to_string()), position: None})
5339    )]
5340    #[case(
5341        Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: None, position: None}),
5342        attr_keys::LABEL,
5343        "new_label",
5344        Node::ImageRef(ImageRef{alt: attr_keys::ALT.to_string(), ident: "id".to_string(), label: Some("new_label".to_string()), position: None})
5345    )]
5346    #[case(
5347        Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![], label: Some(attr_keys::LABEL.to_string()), position: None}),
5348        attr_keys::IDENT,
5349        "new_id",
5350        Node::LinkRef(LinkRef{ident: "new_id".to_string(), values: vec![], label: Some(attr_keys::LABEL.to_string()), position: None})
5351    )]
5352    #[case(
5353        Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![], label: Some(attr_keys::LABEL.to_string()), position: None}),
5354        attr_keys::LABEL,
5355        "new_label",
5356        Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![], label: Some("new_label".to_string()), position: None})
5357    )]
5358    #[case(
5359        Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![], label: None, position: None}),
5360        attr_keys::LABEL,
5361        "new_label",
5362        Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![], label: Some("new_label".to_string()), position: None})
5363    )]
5364    #[case(
5365        Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![], label: Some(attr_keys::LABEL.to_string()), position: None}),
5366        "unknown",
5367        "ignored",
5368        Node::LinkRef(LinkRef{ident: "id".to_string(), values: vec![], label: Some(attr_keys::LABEL.to_string()), position: None})
5369    )]
5370    #[case(
5371        Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}),
5372        attr_keys::IDENT,
5373        "new_id",
5374        Node::FootnoteRef(FootnoteRef{ident: "new_id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None})
5375    )]
5376    #[case(
5377        Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}),
5378        attr_keys::LABEL,
5379        "new_label",
5380        Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: Some("new_label".to_string()), position: None})
5381    )]
5382    #[case(
5383        Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: None, position: None}),
5384        attr_keys::LABEL,
5385        "new_label",
5386        Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: Some("new_label".to_string()), position: None})
5387    )]
5388    #[case(
5389        Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None}),
5390        "unknown",
5391        "ignored",
5392        Node::FootnoteRef(FootnoteRef{ident: "id".to_string(), label: Some(attr_keys::LABEL.to_string()), position: None})
5393    )]
5394    #[case(Node::Empty, attr_keys::VALUE, "ignored", Node::Empty)]
5395    #[case(
5396        Node::TableAlign(TableAlign{align: vec![TableAlignKind::Left, TableAlignKind::Right], position: None}),
5397        "align",
5398        "---,:---:",
5399        Node::TableAlign(TableAlign{align: vec![TableAlignKind::None, TableAlignKind::Center], position: None})
5400    )]
5401    #[case(
5402        Node::TableAlign(TableAlign{align: vec![], position: None}),
5403        "align",
5404        ":---,---:",
5405        Node::TableAlign(TableAlign{align: vec![TableAlignKind::Left, TableAlignKind::Right], position: None})
5406    )]
5407    #[case(
5408        Node::TableAlign(TableAlign{align: vec![TableAlignKind::Left], position: None}),
5409        "unknown",
5410        "ignored",
5411        Node::TableAlign(TableAlign{align: vec![TableAlignKind::Left], position: None})
5412    )]
5413    #[case(
5414        Node::MdxFlowExpression(MdxFlowExpression{value: "old".into(), position: None}),
5415        attr_keys::VALUE,
5416        "new_expr",
5417        Node::MdxFlowExpression(MdxFlowExpression{value: "new_expr".into(), position: None})
5418    )]
5419    #[case(
5420        Node::MdxFlowExpression(MdxFlowExpression{value: "expr".into(), position: None}),
5421        "unknown",
5422        "ignored",
5423        Node::MdxFlowExpression(MdxFlowExpression{value: "expr".into(), position: None})
5424    )]
5425    #[case(
5426        Node::MdxTextExpression(MdxTextExpression{value: "old".into(), position: None}),
5427        attr_keys::VALUE,
5428        "new_expr",
5429        Node::MdxTextExpression(MdxTextExpression{value: "new_expr".into(), position: None})
5430    )]
5431    #[case(
5432        Node::MdxTextExpression(MdxTextExpression{value: "expr".into(), position: None}),
5433        "unknown",
5434        "ignored",
5435        Node::MdxTextExpression(MdxTextExpression{value: "expr".into(), position: None})
5436    )]
5437    #[case(
5438        Node::MdxJsEsm(MdxJsEsm{value: "import x".into(), position: None}),
5439        attr_keys::VALUE,
5440        "import y",
5441        Node::MdxJsEsm(MdxJsEsm{value: "import y".into(), position: None})
5442    )]
5443    #[case(
5444        Node::MdxJsEsm(MdxJsEsm{value: "import x".into(), position: None}),
5445        "unknown",
5446        "ignored",
5447        Node::MdxJsEsm(MdxJsEsm{value: "import x".into(), position: None})
5448    )]
5449    #[case(
5450        Node::MdxJsxFlowElement(MdxJsxFlowElement{name: Some("div".to_string()), attributes: Vec::new(), children: Vec::new(), position: None}),
5451        attr_keys::NAME,
5452        "section",
5453        Node::MdxJsxFlowElement(MdxJsxFlowElement{name: Some("section".to_string()), attributes: Vec::new(), children: Vec::new(), position: None})
5454    )]
5455    #[case(
5456        Node::MdxJsxFlowElement(MdxJsxFlowElement{name: None, attributes: Vec::new(), children: Vec::new(), position: None}),
5457        attr_keys::NAME,
5458        "main",
5459        Node::MdxJsxFlowElement(MdxJsxFlowElement{name: Some("main".to_string()), attributes: Vec::new(), children: Vec::new(), position: None})
5460    )]
5461    #[case(
5462        Node::MdxJsxFlowElement(MdxJsxFlowElement{name: Some("div".to_string()), attributes: Vec::new(), children: Vec::new(), position: None}),
5463        "unknown",
5464        "ignored",
5465        Node::MdxJsxFlowElement(MdxJsxFlowElement{name: Some("div".to_string()), attributes: Vec::new(), children: Vec::new(), position: None})
5466    )]
5467    #[case(
5468        Node::MdxJsxTextElement(MdxJsxTextElement{name: Some("span".into()), attributes: Vec::new(), children: Vec::new(), position: None}),
5469        attr_keys::NAME,
5470        "b",
5471        Node::MdxJsxTextElement(MdxJsxTextElement{name: Some("b".into()), attributes: Vec::new(), children: Vec::new(), position: None})
5472    )]
5473    #[case(
5474        Node::MdxJsxTextElement(MdxJsxTextElement{name: None, attributes: Vec::new(), children: Vec::new(), position: None}),
5475        attr_keys::NAME,
5476        "i",
5477        Node::MdxJsxTextElement(MdxJsxTextElement{name: Some("i".into()), attributes: Vec::new(), children: Vec::new(), position: None})
5478    )]
5479    #[case(
5480        Node::MdxJsxTextElement(MdxJsxTextElement{name: Some("span".into()), attributes: Vec::new(), children: Vec::new(), position: None}),
5481        "unknown",
5482        "ignored",
5483        Node::MdxJsxTextElement(MdxJsxTextElement{name: Some("span".into()), attributes: Vec::new(), children: Vec::new(), position: None})
5484    )]
5485    fn test_set_attr(#[case] mut node: Node, #[case] attr: &str, #[case] value: &str, #[case] expected: Node) {
5486        node.set_attr(attr, value);
5487        assert_eq!(node, expected);
5488    }
5489
5490    #[rstest]
5491    #[case(AttrValue::String("test".to_string()), AttrValue::String("test".to_string()), true)]
5492    #[case(AttrValue::String("test".to_string()), AttrValue::String("other".to_string()), false)]
5493    #[case(AttrValue::Integer(42), AttrValue::Integer(42), true)]
5494    #[case(AttrValue::Integer(42), AttrValue::Integer(0), false)]
5495    #[case(AttrValue::Boolean(true), AttrValue::Boolean(true), true)]
5496    #[case(AttrValue::Boolean(true), AttrValue::Boolean(false), false)]
5497    #[case(AttrValue::String("42".to_string()), AttrValue::Integer(42), false)]
5498    #[case(AttrValue::Boolean(false), AttrValue::Integer(0), false)]
5499    fn test_attr_value_eq(#[case] a: AttrValue, #[case] b: AttrValue, #[case] expected: bool) {
5500        assert_eq!(a == b, expected);
5501    }
5502
5503    #[rstest]
5504    #[case(AttrValue::String("test".to_string()), "test")]
5505    #[case(AttrValue::Integer(42), "42")]
5506    #[case(AttrValue::Boolean(true), "true")]
5507    fn test_attr_value_as_str(#[case] value: AttrValue, #[case] expected: &str) {
5508        assert_eq!(&value.as_string(), expected);
5509    }
5510
5511    #[rstest]
5512    #[case(AttrValue::Integer(42), Some(42))]
5513    #[case(AttrValue::String("42".to_string()), Some(42))]
5514    #[case(AttrValue::Boolean(false), Some(0))]
5515    fn test_attr_value_as_i64(#[case] value: AttrValue, #[case] expected: Option<i64>) {
5516        assert_eq!(value.as_i64(), expected);
5517    }
5518
5519    // --- Callout tests ---
5520
5521    #[cfg(feature = "callout")]
5522    #[rstest]
5523    // basic [!NOTE] with body on next line
5524    #[case(
5525        vec![Node::Text(Text { value: "[!NOTE]\nbody text".to_string(), position: None })],
5526        Node::Callout(Callout {
5527            kind: "NOTE".to_string(),
5528            title: None,
5529            values: vec![Node::Text(Text { value: "body text".to_string(), position: None })],
5530            position: None,
5531        })
5532    )]
5533    // [!WARNING] with custom title
5534    #[case(
5535        vec![Node::Text(Text { value: "[!WARNING] My Title\nbody".to_string(), position: None })],
5536        Node::Callout(Callout {
5537            kind: "WARNING".to_string(),
5538            title: Some("My Title".to_string()),
5539            values: vec![Node::Text(Text { value: "body".to_string(), position: None })],
5540            position: None,
5541        })
5542    )]
5543    // [!TIP] with no body
5544    #[case(
5545        vec![Node::Text(Text { value: "[!TIP]".to_string(), position: None })],
5546        Node::Callout(Callout { kind: "TIP".to_string(), title: None, values: vec![], position: None })
5547    )]
5548    // lowercase kind is preserved as-is
5549    #[case(
5550        vec![Node::Text(Text { value: "[!note]\ncontent".to_string(), position: None })],
5551        Node::Callout(Callout {
5552            kind: "note".to_string(),
5553            title: None,
5554            values: vec![Node::Text(Text { value: "content".to_string(), position: None })],
5555            position: None,
5556        })
5557    )]
5558    // not a callout — plain text stays Blockquote
5559    #[case(
5560        vec![Node::Text(Text { value: "plain quote".to_string(), position: None })],
5561        Node::Blockquote(Blockquote {
5562            values: vec![Node::Text(Text { value: "plain quote".to_string(), position: None })],
5563            position: None,
5564        })
5565    )]
5566    // [! without closing ] — stays Blockquote
5567    #[case(
5568        vec![Node::Text(Text { value: "[!UNCLOSED".to_string(), position: None })],
5569        Node::Blockquote(Blockquote {
5570            values: vec![Node::Text(Text { value: "[!UNCLOSED".to_string(), position: None })],
5571            position: None,
5572        })
5573    )]
5574    // first node is not Text — stays Blockquote
5575    #[case(
5576        vec![Node::Code(Code { value: "code".to_string(), lang: None, fence: true, meta: None, position: None })],
5577        Node::Blockquote(Blockquote {
5578            values: vec![Node::Code(Code { value: "code".to_string(), lang: None, fence: true, meta: None, position: None })],
5579            position: None,
5580        })
5581    )]
5582    // multi-paragraph callout (header-only node + separate body node)
5583    #[case(
5584        vec![
5585            Node::Text(Text { value: "[!NOTE]".to_string(), position: None }),
5586            Node::Text(Text { value: "second paragraph".to_string(), position: None }),
5587        ],
5588        Node::Callout(Callout {
5589            kind: "NOTE".to_string(),
5590            title: None,
5591            values: vec![Node::Text(Text { value: "second paragraph".to_string(), position: None })],
5592            position: None,
5593        })
5594    )]
5595    fn test_try_parse_callout(#[case] values: Vec<Node>, #[case] expected: Node) {
5596        let result = Node::try_parse_callout(values, None);
5597        assert_eq!(result, expected);
5598    }
5599
5600    #[cfg(feature = "callout")]
5601    #[rstest]
5602    // basic render
5603    #[case(
5604        Node::Callout(Callout { kind: "NOTE".to_string(), title: None,
5605            values: vec![Node::Text(Text { value: "content".to_string(), position: None })],
5606            position: None }),
5607        "> [!NOTE]\n> content"
5608    )]
5609    // with title
5610    #[case(
5611        Node::Callout(Callout { kind: "WARNING".to_string(), title: Some("Heads up".to_string()),
5612            values: vec![Node::Text(Text { value: "watch out".to_string(), position: None })],
5613            position: None }),
5614        "> [!WARNING] Heads up\n> watch out"
5615    )]
5616    // empty body
5617    #[case(
5618        Node::Callout(Callout { kind: "TIP".to_string(), title: None, values: vec![], position: None }),
5619        "> [!TIP]"
5620    )]
5621    // multiline body: single Text with embedded '\n' — each line prefixed with "> "
5622    #[case(
5623        Node::Callout(Callout { kind: "INFO".to_string(), title: None,
5624            values: vec![Node::Text(Text { value: "line one\nline two".to_string(), position: None })],
5625            position: None }),
5626        "> [!INFO]\n> line one\n> line two"
5627    )]
5628    // two separate position-less Text values are concatenated inline (no implicit newline)
5629    #[case(
5630        Node::Callout(Callout { kind: "INFO".to_string(), title: None,
5631            values: vec![
5632                Node::Text(Text { value: "part a".to_string(), position: None }),
5633                Node::Text(Text { value: "part b".to_string(), position: None }),
5634            ],
5635            position: None }),
5636        "> [!INFO]\n> part apart b"
5637    )]
5638    fn test_callout_render(#[case] node: Node, #[case] expected: &str) {
5639        assert_eq!(node.to_string_with(&RenderOptions::default()), expected);
5640    }
5641
5642    #[cfg(feature = "callout")]
5643    #[rstest]
5644    #[case(Node::Callout(Callout { kind: "NOTE".to_string(), title: None, values: vec![], position: None }), "kind", Some(AttrValue::String("NOTE".to_string())))]
5645    #[case(Node::Callout(Callout { kind: "WARNING".to_string(), title: Some("Title".to_string()), values: vec![], position: None }), "title", Some(AttrValue::String("Title".to_string())))]
5646    #[case(Node::Callout(Callout { kind: "TIP".to_string(), title: None, values: vec![], position: None }), "title", None)]
5647    #[case(Node::Callout(Callout { kind: "NOTE".to_string(), title: None, values: vec![Node::Text(Text { value: "body".to_string(), position: None })], position: None }), "value", Some(AttrValue::String("body".to_string())))]
5648    #[case(Node::Callout(Callout { kind: "NOTE".to_string(), title: None, values: vec![Node::Text(Text { value: "body".to_string(), position: None })], position: None }), "children", Some(AttrValue::Array(vec![Node::Text(Text { value: "body".to_string(), position: None })])))]
5649    fn test_callout_attr(#[case] node: Node, #[case] attr: &str, #[case] expected: Option<AttrValue>) {
5650        assert_eq!(node.attr(attr), expected);
5651    }
5652
5653    #[cfg(feature = "callout")]
5654    #[rstest]
5655    #[case(
5656        Node::Callout(Callout { kind: "NOTE".to_string(), title: None, values: vec![], position: None }),
5657        "kind", "WARNING",
5658        Node::Callout(Callout { kind: "WARNING".to_string(), title: None, values: vec![], position: None })
5659    )]
5660    // set_attr stores kind as-is without case conversion
5661    #[case(
5662        Node::Callout(Callout { kind: "NOTE".to_string(), title: None, values: vec![], position: None }),
5663        "kind", "tip",
5664        Node::Callout(Callout { kind: "tip".to_string(), title: None, values: vec![], position: None })
5665    )]
5666    #[case(
5667        Node::Callout(Callout { kind: "NOTE".to_string(), title: None, values: vec![], position: None }),
5668        "title", "My title",
5669        Node::Callout(Callout { kind: "NOTE".to_string(), title: Some("My title".to_string()), values: vec![], position: None })
5670    )]
5671    // empty string clears title
5672    #[case(
5673        Node::Callout(Callout { kind: "NOTE".to_string(), title: Some("old".to_string()), values: vec![], position: None }),
5674        "title", "",
5675        Node::Callout(Callout { kind: "NOTE".to_string(), title: None, values: vec![], position: None })
5676    )]
5677    fn test_callout_set_attr(#[case] mut node: Node, #[case] attr: &str, #[case] value: &str, #[case] expected: Node) {
5678        node.set_attr(attr, value);
5679        assert_eq!(node, expected);
5680    }
5681
5682    #[cfg(feature = "callout")]
5683    #[rstest]
5684    // with_value changes first body node
5685    #[case(
5686        Node::Callout(Callout { kind: "NOTE".to_string(), title: None,
5687            values: vec![Node::Text(Text { value: "old".to_string(), position: None })],
5688            position: None }),
5689        "new",
5690        Node::Callout(Callout { kind: "NOTE".to_string(), title: None,
5691            values: vec![Node::Text(Text { value: "new".to_string(), position: None })],
5692            position: None })
5693    )]
5694    fn test_callout_with_value(#[case] node: Node, #[case] value: &str, #[case] expected: Node) {
5695        assert_eq!(node.with_value(value), expected);
5696    }
5697
5698    #[cfg(feature = "callout")]
5699    #[rstest]
5700    #[case(Node::Callout(Callout { kind: "NOTE".to_string(), title: None, values: vec![], position: None }), "callout")]
5701    fn test_callout_name(#[case] node: Node, #[case] expected: &str) {
5702        assert_eq!(node.name(), expected);
5703    }
5704
5705    #[cfg(feature = "callout")]
5706    #[rstest]
5707    #[case(
5708        Node::Callout(Callout { kind: "NOTE".to_string(), title: None,
5709            values: vec![Node::Text(Text { value: "body".to_string(), position: None })],
5710            position: None }),
5711        "body"
5712    )]
5713    fn test_callout_value(#[case] node: Node, #[case] expected: &str) {
5714        assert_eq!(node.value(), expected);
5715    }
5716
5717    #[cfg(feature = "callout")]
5718    #[rstest]
5719    #[case(Node::Callout(Callout { kind: "NOTE".to_string(), title: None, values: vec![], position: None }), true)]
5720    #[case(Node::Blockquote(Blockquote { values: vec![], position: None }), false)]
5721    #[case(Node::Text(Text { value: "test".to_string(), position: None }), false)]
5722    fn test_is_callout(#[case] node: Node, #[case] expected: bool) {
5723        assert_eq!(node.is_callout(), expected);
5724    }
5725
5726    #[cfg(feature = "callout")]
5727    #[test]
5728    fn test_callout_end_to_end() {
5729        use crate::Markdown;
5730        // plain blockquote stays as Blockquote
5731        let plain = Markdown::from_markdown_str("> just a quote").unwrap();
5732        assert!(plain.nodes[0].is_blockquote());
5733
5734        // [!NOTE] is parsed as Callout
5735        let note = Markdown::from_markdown_str("> [!NOTE]\n> body").unwrap();
5736        assert!(note.nodes[0].is_callout());
5737        assert_eq!(note.nodes[0].attr("kind"), Some(AttrValue::String("NOTE".to_string())));
5738
5739        // [!WARNING] with title
5740        let warn = Markdown::from_markdown_str("> [!WARNING] Watch out\n> content").unwrap();
5741        assert!(warn.nodes[0].is_callout());
5742        assert_eq!(
5743            warn.nodes[0].attr("title"),
5744            Some(AttrValue::String("Watch out".to_string()))
5745        );
5746    }
5747
5748    // --- Embed tests ---
5749
5750    #[cfg(feature = "embed")]
5751    #[rstest]
5752    // plain embed
5753    #[case("![[target]]", vec![Node::Embed(Embed { target: "target".to_string(), display: None, position: None })])]
5754    // embed with display hint (image size)
5755    #[case("![[image.png|400]]", vec![Node::Embed(Embed { target: "image.png".to_string(), display: Some("400".to_string()), position: None })])]
5756    // embed with section reference in target
5757    #[case("![[note#Heading]]", vec![Node::Embed(Embed { target: "note#Heading".to_string(), display: None, position: None })])]
5758    // embed at start of text
5759    #[case("![[note]] rest", vec![
5760        Node::Embed(Embed { target: "note".to_string(), display: None, position: None }),
5761        Node::Text(Text { value: " rest".to_string(), position: None }),
5762    ])]
5763    // embed in middle
5764    #[case("before ![[note]] after", vec![
5765        Node::Text(Text { value: "before ".to_string(), position: None }),
5766        Node::Embed(Embed { target: "note".to_string(), display: None, position: None }),
5767        Node::Text(Text { value: " after".to_string(), position: None }),
5768    ])]
5769    // multiple embeds
5770    #[case("![[a]] and ![[b]]", vec![
5771        Node::Embed(Embed { target: "a".to_string(), display: None, position: None }),
5772        Node::Text(Text { value: " and ".to_string(), position: None }),
5773        Node::Embed(Embed { target: "b".to_string(), display: None, position: None }),
5774    ])]
5775    // multibyte target
5776    #[case("![[ノート]]", vec![Node::Embed(Embed { target: "ノート".to_string(), display: None, position: None })])]
5777    // no embed — plain text unchanged
5778    #[case("plain text", vec![Node::Text(Text { value: "plain text".to_string(), position: None })])]
5779    // unclosed embed — treated as plain text
5780    #[case("![[unclosed", vec![Node::Text(Text { value: "![[unclosed".to_string(), position: None })])]
5781    // lone ! before non-embed — treated as plain text
5782    #[case("! not an embed", vec![Node::Text(Text { value: "! not an embed".to_string(), position: None })])]
5783    fn test_parse_embeds_in_text(#[case] input: &str, #[case] expected: Vec<Node>) {
5784        let mut result = Vec::new();
5785        Node::parse_embeds_into(input, None, &mut result);
5786        assert_eq!(result, expected);
5787    }
5788
5789    #[cfg(feature = "embed")]
5790    #[rstest]
5791    // plain embed renders without display
5792    #[case(
5793        Node::Embed(Embed { target: "note.md".to_string(), display: None, position: None }),
5794        "![[note.md]]"
5795    )]
5796    // embed with display hint
5797    #[case(
5798        Node::Embed(Embed { target: "image.png".to_string(), display: Some("400".to_string()), position: None }),
5799        "![[image.png|400]]"
5800    )]
5801    // embed with section reference
5802    #[case(
5803        Node::Embed(Embed { target: "note#Intro".to_string(), display: None, position: None }),
5804        "![[note#Intro]]"
5805    )]
5806    fn test_embed_render(#[case] node: Node, #[case] expected: &str) {
5807        assert_eq!(node.to_string_with(&RenderOptions::default()), expected);
5808    }
5809
5810    #[cfg(feature = "embed")]
5811    #[rstest]
5812    #[case(Node::Embed(Embed { target: "note".to_string(), display: None, position: None }), "url", Some(AttrValue::String("note".to_string())))]
5813    // value returns target when no display
5814    #[case(Node::Embed(Embed { target: "note".to_string(), display: None, position: None }), "value", Some(AttrValue::String("note".to_string())))]
5815    // url always returns target regardless of display
5816    #[case(Node::Embed(Embed { target: "note".to_string(), display: Some("400".to_string()), position: None }), "url", Some(AttrValue::String("note".to_string())))]
5817    // value returns display when present
5818    #[case(Node::Embed(Embed { target: "note".to_string(), display: Some("400".to_string()), position: None }), "value", Some(AttrValue::String("400".to_string())))]
5819    // unknown attr returns None
5820    #[case(Node::Embed(Embed { target: "note".to_string(), display: None, position: None }), "unknown", None)]
5821    fn test_embed_attr(#[case] node: Node, #[case] attr: &str, #[case] expected: Option<AttrValue>) {
5822        assert_eq!(node.attr(attr), expected);
5823    }
5824
5825    #[cfg(feature = "embed")]
5826    #[rstest]
5827    // set url changes target
5828    #[case(
5829        Node::Embed(Embed { target: "old".to_string(), display: None, position: None }),
5830        "url", "new",
5831        Node::Embed(Embed { target: "new".to_string(), display: None, position: None })
5832    )]
5833    // set value changes display
5834    #[case(
5835        Node::Embed(Embed { target: "note".to_string(), display: None, position: None }),
5836        "value", "800",
5837        Node::Embed(Embed { target: "note".to_string(), display: Some("800".to_string()), position: None })
5838    )]
5839    // empty value clears display
5840    #[case(
5841        Node::Embed(Embed { target: "note".to_string(), display: Some("400".to_string()), position: None }),
5842        "value", "",
5843        Node::Embed(Embed { target: "note".to_string(), display: None, position: None })
5844    )]
5845    fn test_embed_set_attr(#[case] mut node: Node, #[case] attr: &str, #[case] value: &str, #[case] expected: Node) {
5846        node.set_attr(attr, value);
5847        assert_eq!(node, expected);
5848    }
5849
5850    #[cfg(feature = "embed")]
5851    #[rstest]
5852    // no display: with_value sets target
5853    #[case(
5854        Node::Embed(Embed { target: "old".to_string(), display: None, position: None }),
5855        "new",
5856        Node::Embed(Embed { target: "new".to_string(), display: None, position: None })
5857    )]
5858    // with display: with_value sets display
5859    #[case(
5860        Node::Embed(Embed { target: "note".to_string(), display: Some("400".to_string()), position: None }),
5861        "800",
5862        Node::Embed(Embed { target: "note".to_string(), display: Some("800".to_string()), position: None })
5863    )]
5864    fn test_embed_with_value(#[case] node: Node, #[case] value: &str, #[case] expected: Node) {
5865        assert_eq!(node.with_value(value), expected);
5866    }
5867
5868    #[cfg(feature = "embed")]
5869    #[rstest]
5870    #[case(Node::Embed(Embed { target: "note".to_string(), display: None, position: None }), "embed")]
5871    fn test_embed_name(#[case] node: Node, #[case] expected: &str) {
5872        assert_eq!(node.name(), expected);
5873    }
5874
5875    #[cfg(feature = "embed")]
5876    #[rstest]
5877    // value returns display when present
5878    #[case(Node::Embed(Embed { target: "note".to_string(), display: Some("400".to_string()), position: None }), "400")]
5879    // value returns target when no display
5880    #[case(Node::Embed(Embed { target: "note".to_string(), display: None, position: None }), "note")]
5881    fn test_embed_value(#[case] node: Node, #[case] expected: &str) {
5882        assert_eq!(node.value(), expected);
5883    }
5884
5885    #[cfg(feature = "embed")]
5886    #[rstest]
5887    #[case(Node::Embed(Embed { target: "note".to_string(), display: None, position: None }), true)]
5888    #[case(Node::Text(Text { value: "test".to_string(), position: None }), false)]
5889    #[case(Node::Image(Image { alt: "".to_string(), url: "img.png".to_string(), title: None, position: None }), false)]
5890    fn test_is_embed(#[case] node: Node, #[case] expected: bool) {
5891        assert_eq!(node.is_embed(), expected);
5892    }
5893
5894    #[cfg(feature = "embed")]
5895    #[test]
5896    fn test_embed_end_to_end() {
5897        use crate::Markdown;
5898        let md = Markdown::from_markdown_str("See ![[note.md]] for details.").unwrap();
5899        let embed = md.nodes.iter().find(|n| n.is_embed());
5900        assert!(embed.is_some());
5901        assert_eq!(
5902            embed.unwrap().attr("url"),
5903            Some(AttrValue::String("note.md".to_string()))
5904        );
5905    }
5906
5907    #[cfg(all(feature = "callout", feature = "wikilink"))]
5908    #[rstest]
5909    // callout body containing a wikilink
5910    #[case("> [!NOTE]\n> See [[note]]", 1, "> [!NOTE]\n> See [[note]]\n")]
5911    fn test_callout_with_wikilink(#[case] input: &str, #[case] expected_nodes: usize, #[case] expected_output: &str) {
5912        use crate::Markdown;
5913        let md = Markdown::from_markdown_str(input).unwrap();
5914        assert_eq!(md.nodes.len(), expected_nodes);
5915        assert_eq!(md.to_string(), expected_output);
5916    }
5917
5918    #[cfg(feature = "callout")]
5919    #[rstest]
5920    // callout body with bold
5921    #[case("> [!NOTE]\n> **important**", 1, "> [!NOTE]\n> **important**\n")]
5922    // callout body with inline code
5923    #[case("> [!NOTE]\n> Use `code`", 1, "> [!NOTE]\n> Use `code`\n")]
5924    // multiple callouts in one document
5925    #[case(
5926        "> [!NOTE]\n> first\n\n> [!WARNING]\n> second",
5927        2,
5928        "> [!NOTE]\n> first\n\n> [!WARNING]\n> second\n"
5929    )]
5930    // callout after heading
5931    #[case("# Title\n\n> [!NOTE]\n> body", 2, "# Title\n\n> [!NOTE]\n> body\n")]
5932    // callout body with a multi-item list — second item must not be over-indented
5933    #[case("> [!NOTE]\n> - item 1\n> - item 2", 1, "> [!NOTE]\n> - item 1\n> - item 2\n")]
5934    fn test_callout_combined_with_other_nodes(
5935        #[case] input: &str,
5936        #[case] expected_nodes: usize,
5937        #[case] expected_output: &str,
5938    ) {
5939        use crate::Markdown;
5940        let md = Markdown::from_markdown_str(input).unwrap();
5941        assert_eq!(md.nodes.len(), expected_nodes);
5942        assert_eq!(md.to_string(), expected_output);
5943    }
5944
5945    #[cfg(feature = "embed")]
5946    #[rstest]
5947    // embed inside list item
5948    #[case("- ![[note.md]]", 1, "- ![[note.md]]\n")]
5949    // embed inside heading
5950    #[case("# See ![[note]]", 1, "# See ![[note]]\n")]
5951    // embed inside bold
5952    #[case("**![[note]]**", 1, "**![[note]]**\n")]
5953    fn test_embed_combined_with_other_nodes(
5954        #[case] input: &str,
5955        #[case] expected_nodes: usize,
5956        #[case] expected_output: &str,
5957    ) {
5958        use crate::Markdown;
5959        let md = Markdown::from_markdown_str(input).unwrap();
5960        assert_eq!(md.nodes.len(), expected_nodes);
5961        assert_eq!(md.to_string(), expected_output);
5962    }
5963
5964    #[cfg(all(feature = "callout", feature = "embed"))]
5965    #[rstest]
5966    // wikilink inside callout body + embed as separate node
5967    #[case("> [!NOTE]\n> [[link]]\n\n![[embed]]", 2, "> [!NOTE]\n> [[link]]\n\n![[embed]]\n")]
5968    fn test_callout_and_embed_together(
5969        #[case] input: &str,
5970        #[case] expected_nodes: usize,
5971        #[case] expected_output: &str,
5972    ) {
5973        use crate::Markdown;
5974        let md = Markdown::from_markdown_str(input).unwrap();
5975        assert_eq!(md.nodes.len(), expected_nodes);
5976        assert_eq!(md.to_string(), expected_output);
5977    }
5978
5979    #[cfg(all(feature = "embed", feature = "wikilink"))]
5980    #[test]
5981    fn test_embed_does_not_conflict_with_wikilink() {
5982        use crate::Markdown;
5983        // ![[embed]] must become Embed, not WikiLink with preceding "!"
5984        let md = Markdown::from_markdown_str("![[embed]] and [[link]]").unwrap();
5985        let embed = md.nodes.iter().find(|n| n.is_embed());
5986        let wikilink = md.nodes.iter().find(|n| n.is_wikilink());
5987        assert!(embed.is_some(), "expected Embed node");
5988        assert!(wikilink.is_some(), "expected WikiLink node");
5989    }
5990}