1use super::html::HtmlElement;
4use crate::traits::CustomNode;
5use ecow::EcoString;
6use std::boxed::Box;
7
8#[derive(Debug, Clone, Copy, PartialEq, Default)]
10pub enum CodeBlockType {
11 Indented,
13 #[default]
15 Fenced,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Default)]
20pub enum HeadingType {
21 #[default]
23 Atx,
24 Setext,
26}
27
28#[cfg(feature = "gfm")]
30#[derive(Debug, Clone, PartialEq, Default)]
31pub enum TableAlignment {
32 #[default]
34 Left,
35 Center,
37 Right,
39 None,
41}
42
43#[cfg(feature = "gfm")]
45#[derive(Debug, Clone, PartialEq)]
46pub enum TaskListStatus {
47 Checked,
49 Unchecked,
51}
52
53#[derive(Debug)]
55pub enum Node {
56 Document(Vec<Node>),
58
59 ThematicBreak,
63
64 Heading {
67 level: u8,
69 content: Vec<Node>,
71 heading_type: HeadingType,
73 },
74
75 CodeBlock {
78 language: Option<EcoString>,
80 content: EcoString,
82 block_type: CodeBlockType,
84 },
85
86 HtmlBlock(EcoString),
89
90 LinkReferenceDefinition {
93 label: EcoString,
95 destination: EcoString,
97 title: Option<EcoString>,
99 },
100
101 Paragraph(Vec<Node>),
104
105 BlockQuote(Vec<Node>),
111
112 OrderedList {
115 start: u32,
117 items: Vec<ListItem>,
119 },
120
121 UnorderedList(Vec<ListItem>),
123
124 Table {
126 headers: Vec<Node>,
128 #[cfg(feature = "gfm")]
130 alignments: Vec<TableAlignment>,
131 rows: Vec<Vec<Node>>,
133 },
134
135 InlineCode(EcoString),
139
140 Emphasis(Vec<Node>),
143
144 Strong(Vec<Node>),
146
147 Strikethrough(Vec<Node>),
149
150 Link {
153 url: EcoString,
155 title: Option<EcoString>,
157 content: Vec<Node>,
159 },
160
161 ReferenceLink {
163 label: EcoString,
165 content: Vec<Node>,
167 },
168
169 Image {
172 url: EcoString,
174 title: Option<EcoString>,
176 alt: Vec<Node>,
178 },
179
180 Autolink {
183 url: EcoString,
185 is_email: bool,
187 },
188
189 ExtendedAutolink(EcoString),
191
192 HtmlElement(HtmlElement),
195
196 HardBreak,
199
200 SoftBreak,
203
204 Text(EcoString),
207
208 Custom(Box<dyn CustomNode>),
210}
211
212impl Default for Node {
213 fn default() -> Self {
214 Node::Document(vec![])
215 }
216}
217
218impl Clone for Node {
219 fn clone(&self) -> Self {
220 match self {
221 Node::Document(nodes) => Node::Document(nodes.clone()),
222 Node::ThematicBreak => Node::ThematicBreak,
223 Node::Heading {
224 level,
225 content,
226 heading_type,
227 } => Node::Heading {
228 level: *level,
229 content: content.clone(),
230 heading_type: *heading_type,
231 },
232 Node::CodeBlock {
233 language,
234 content,
235 block_type,
236 } => Node::CodeBlock {
237 language: language.clone(),
238 content: content.clone(),
239 block_type: *block_type,
240 },
241 Node::HtmlBlock(html) => Node::HtmlBlock(html.clone()),
242 Node::LinkReferenceDefinition {
243 label,
244 destination,
245 title,
246 } => Node::LinkReferenceDefinition {
247 label: label.clone(),
248 destination: destination.clone(),
249 title: title.clone(),
250 },
251 Node::Paragraph(content) => Node::Paragraph(content.clone()),
252 Node::BlockQuote(content) => Node::BlockQuote(content.clone()),
253 Node::OrderedList { start, items } => Node::OrderedList {
254 start: *start,
255 items: items.clone(),
256 },
257 Node::UnorderedList(items) => Node::UnorderedList(items.clone()),
258 #[cfg(feature = "gfm")]
259 Node::Table {
260 headers,
261 alignments,
262 rows,
263 } => Node::Table {
264 headers: headers.clone(),
265 alignments: alignments.clone(),
266 rows: rows.clone(),
267 },
268 #[cfg(not(feature = "gfm"))]
269 Node::Table { headers, rows } => Node::Table {
270 headers: headers.clone(),
271 rows: rows.clone(),
272 },
273 Node::InlineCode(code) => Node::InlineCode(code.clone()),
274 Node::Emphasis(content) => Node::Emphasis(content.clone()),
275 Node::Strong(content) => Node::Strong(content.clone()),
276 Node::Strikethrough(content) => Node::Strikethrough(content.clone()),
277 Node::Link {
278 url,
279 title,
280 content,
281 } => Node::Link {
282 url: url.clone(),
283 title: title.clone(),
284 content: content.clone(),
285 },
286 Node::ReferenceLink { label, content } => Node::ReferenceLink {
287 label: label.clone(),
288 content: content.clone(),
289 },
290 Node::Image { url, title, alt } => Node::Image {
291 url: url.clone(),
292 title: title.clone(),
293 alt: alt.clone(),
294 },
295 Node::Autolink { url, is_email } => Node::Autolink {
296 url: url.clone(),
297 is_email: *is_email,
298 },
299 Node::ExtendedAutolink(url) => Node::ExtendedAutolink(url.clone()),
300 Node::HtmlElement(element) => Node::HtmlElement(element.clone()),
301 Node::HardBreak => Node::HardBreak,
302 Node::SoftBreak => Node::SoftBreak,
303 Node::Text(text) => Node::Text(text.clone()),
304 Node::Custom(_custom) => {
305 panic!("Custom node cloning not supported in simplified design")
308 }
309 }
310 }
311}
312
313impl PartialEq for Node {
314 fn eq(&self, other: &Self) -> bool {
315 match (self, other) {
316 (Node::Document(a), Node::Document(b)) => a == b,
317 (Node::ThematicBreak, Node::ThematicBreak) => true,
318 (
319 Node::Heading {
320 level: l1,
321 content: c1,
322 heading_type: h1,
323 },
324 Node::Heading {
325 level: l2,
326 content: c2,
327 heading_type: h2,
328 },
329 ) => l1 == l2 && c1 == c2 && h1 == h2,
330 (
331 Node::CodeBlock {
332 language: l1,
333 content: c1,
334 block_type: b1,
335 },
336 Node::CodeBlock {
337 language: l2,
338 content: c2,
339 block_type: b2,
340 },
341 ) => l1 == l2 && c1 == c2 && b1 == b2,
342 (Node::HtmlBlock(a), Node::HtmlBlock(b)) => a == b,
343 (
344 Node::LinkReferenceDefinition {
345 label: l1,
346 destination: d1,
347 title: t1,
348 },
349 Node::LinkReferenceDefinition {
350 label: l2,
351 destination: d2,
352 title: t2,
353 },
354 ) => l1 == l2 && d1 == d2 && t1 == t2,
355 (Node::Paragraph(a), Node::Paragraph(b)) => a == b,
356 (Node::BlockQuote(a), Node::BlockQuote(b)) => a == b,
357 (
358 Node::OrderedList {
359 start: s1,
360 items: i1,
361 },
362 Node::OrderedList {
363 start: s2,
364 items: i2,
365 },
366 ) => s1 == s2 && i1 == i2,
367 (Node::UnorderedList(a), Node::UnorderedList(b)) => a == b,
368 #[cfg(feature = "gfm")]
369 (
370 Node::Table {
371 headers: h1,
372 alignments: a1,
373 rows: r1,
374 },
375 Node::Table {
376 headers: h2,
377 alignments: a2,
378 rows: r2,
379 },
380 ) => h1 == h2 && a1 == a2 && r1 == r2,
381 #[cfg(not(feature = "gfm"))]
382 (
383 Node::Table {
384 headers: h1,
385 rows: r1,
386 },
387 Node::Table {
388 headers: h2,
389 rows: r2,
390 },
391 ) => h1 == h2 && r1 == r2,
392 (Node::InlineCode(a), Node::InlineCode(b)) => a == b,
393 (Node::Emphasis(a), Node::Emphasis(b)) => a == b,
394 (Node::Strong(a), Node::Strong(b)) => a == b,
395 #[cfg(feature = "gfm")]
396 (Node::Strikethrough(a), Node::Strikethrough(b)) => a == b,
397 (
398 Node::Link {
399 url: u1,
400 title: t1,
401 content: c1,
402 },
403 Node::Link {
404 url: u2,
405 title: t2,
406 content: c2,
407 },
408 ) => u1 == u2 && t1 == t2 && c1 == c2,
409 (
410 Node::ReferenceLink {
411 label: l1,
412 content: c1,
413 },
414 Node::ReferenceLink {
415 label: l2,
416 content: c2,
417 },
418 ) => l1 == l2 && c1 == c2,
419 (
420 Node::Image {
421 url: u1,
422 title: t1,
423 alt: a1,
424 },
425 Node::Image {
426 url: u2,
427 title: t2,
428 alt: a2,
429 },
430 ) => u1 == u2 && t1 == t2 && a1 == a2,
431 (
432 Node::Autolink {
433 url: u1,
434 is_email: e1,
435 },
436 Node::Autolink {
437 url: u2,
438 is_email: e2,
439 },
440 ) => u1 == u2 && e1 == e2,
441 #[cfg(feature = "gfm")]
442 (Node::ExtendedAutolink(a), Node::ExtendedAutolink(b)) => a == b,
443 (Node::HtmlElement(a), Node::HtmlElement(b)) => a == b,
444 (Node::HardBreak, Node::HardBreak) => true,
445 (Node::SoftBreak, Node::SoftBreak) => true,
446 (Node::Text(a), Node::Text(b)) => a == b,
447 (Node::Custom(a), Node::Custom(b)) => a.eq_box(&**b),
448 _ => false,
449 }
450 }
451}
452
453#[derive(Debug, Clone, PartialEq)]
455pub enum ListItem {
456 Unordered {
458 content: Vec<Node>,
460 },
461 Ordered {
463 number: Option<u32>,
465 content: Vec<Node>,
467 },
468 #[cfg(feature = "gfm")]
470 Task {
471 status: TaskListStatus,
473 content: Vec<Node>,
475 },
476}
477
478impl Node {
479 pub fn is_block(&self) -> bool {
481 matches!(
482 self,
483 Node::Document(_)
484 | Node::ThematicBreak
486 | Node::Heading { .. }
487 | Node::CodeBlock { .. }
488 | Node::HtmlBlock(_)
489 | Node::LinkReferenceDefinition { .. }
490 | Node::Paragraph(_)
491 | Node::BlockQuote(_)
493 | Node::OrderedList { .. }
494 | Node::UnorderedList(_)
495 | Node::Table { .. }
496
497 | Node::Custom(_)
498 )
499 }
500
501 pub fn is_inline(&self) -> bool {
503 matches!(
504 self,
505 Node::InlineCode(_)
508 | Node::Emphasis(_)
510 | Node::Strong(_)
511 | Node::Strikethrough(_)
512 | Node::Link { .. }
514 | Node::ReferenceLink { .. }
515 | Node::Image { .. }
517 | Node::Autolink { .. }
519 | Node::ExtendedAutolink(_)
520 | Node::HtmlElement(_)
522 | Node::HardBreak
524 | Node::SoftBreak
526 | Node::Text(_)
528
529 | Node::Custom(_)
530 )
531 }
532
533 pub fn type_name(&self) -> &'static str {
535 match self {
536 Node::Document(_) => "Document",
537 Node::ThematicBreak => "ThematicBreak",
538 Node::Heading { .. } => "Heading",
539 Node::CodeBlock { .. } => "CodeBlock",
540 Node::HtmlBlock(_) => "HtmlBlock",
541 Node::LinkReferenceDefinition { .. } => "LinkReferenceDefinition",
542 Node::Paragraph(_) => "Paragraph",
543 Node::BlockQuote(_) => "BlockQuote",
544 Node::OrderedList { .. } => "OrderedList",
545 Node::UnorderedList(_) => "UnorderedList",
546 Node::Table { .. } => "Table",
547 Node::InlineCode(_) => "InlineCode",
548 Node::Emphasis(_) => "Emphasis",
549 Node::Strong(_) => "Strong",
550 Node::Strikethrough(_) => "Strikethrough",
551 Node::Link { .. } => "Link",
552 Node::ReferenceLink { .. } => "ReferenceLink",
553 Node::Image { .. } => "Image",
554 Node::Autolink { .. } => "Autolink",
555 Node::ExtendedAutolink(_) => "ExtendedAutolink",
556 Node::HtmlElement(_) => "HtmlElement",
557 Node::HardBreak => "HardBreak",
558 Node::SoftBreak => "SoftBreak",
559 Node::Text(_) => "Text",
560 Node::Custom(_) => "Custom",
561 }
562 }
563 pub fn heading(level: u8, content: Vec<Node>) -> Self {
572 Node::Heading {
573 level,
574 content,
575 heading_type: HeadingType::default(),
576 }
577 }
578
579 pub fn code_block(language: Option<EcoString>, content: EcoString) -> Self {
588 Node::CodeBlock {
589 language,
590 content,
591 block_type: CodeBlockType::default(),
592 }
593 }
594
595 pub fn strikethrough(content: Vec<Node>) -> Self {
603 Node::Strikethrough(content)
604 }
605
606 #[cfg(feature = "gfm")]
615 pub fn task_list_item(status: TaskListStatus, content: Vec<Node>) -> Self {
616 Node::UnorderedList(vec![ListItem::Task { status, content }])
617 }
618
619 #[cfg(feature = "gfm")]
629 pub fn table_with_alignment(
630 headers: Vec<Node>,
631 alignments: Vec<TableAlignment>,
632 rows: Vec<Vec<Node>>,
633 ) -> Self {
634 Node::Table {
635 headers,
636 alignments,
637 rows,
638 }
639 }
640 pub fn as_custom_type<T: CustomNode + 'static>(&self) -> Option<&T> {
642 if let Node::Custom(node) = self {
643 node.as_any().downcast_ref::<T>()
644 } else {
645 None
646 }
647 }
648
649 pub fn is_custom_type<T: CustomNode + 'static>(&self) -> bool {
651 self.as_custom_type::<T>().is_some()
652 }
653}
654
655impl crate::traits::Format<crate::writer::CommonMarkWriter> for Node {
657 fn format(
658 &self,
659 writer: &mut crate::writer::CommonMarkWriter,
660 ) -> crate::error::WriteResult<()> {
661 if self.is_block() {
664 writer.write_node(self)
665 } else {
666 writer.write_node_content(self)
668 }
669 }
670}
671
672impl crate::traits::Format<crate::writer::HtmlWriter> for Node {
673 fn format(&self, writer: &mut crate::writer::HtmlWriter) -> crate::error::WriteResult<()> {
674 writer.write_node_internal(self).map_err(Into::into)
675 }
676}