Skip to main content

use_markdown/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod code_fence;
5pub mod frontmatter;
6pub mod heading;
7pub mod image;
8pub mod link;
9pub mod outline;
10pub mod plain_text;
11
12pub use code_fence::{MarkdownCodeFence, extract_code_fences};
13pub use frontmatter::{extract_frontmatter, has_frontmatter, strip_frontmatter};
14pub use heading::{MarkdownHeading, extract_headings};
15pub use image::{MarkdownImage, extract_images};
16pub use link::{MarkdownLink, extract_links};
17pub use outline::{MarkdownOutline, extract_outline};
18pub use plain_text::markdown_to_plain_text;
19
20/// Converts heading text into a practical GitHub-style anchor.
21pub fn heading_to_anchor(text: &str) -> String {
22    let mut anchor = String::new();
23    let mut previous_was_separator = false;
24
25    for character in text.trim().chars() {
26        if character.is_alphanumeric() {
27            for lowered in character.to_lowercase() {
28                anchor.push(lowered);
29            }
30            previous_was_separator = false;
31            continue;
32        }
33
34        if matches!(character, '\'' | '"' | '`') {
35            continue;
36        }
37
38        if !anchor.is_empty() && !previous_was_separator {
39            anchor.push('-');
40            previous_was_separator = true;
41        }
42    }
43
44    anchor.trim_matches('-').to_owned()
45}
46
47/// Returns `true` when a line looks like a Markdown horizontal rule.
48pub fn is_horizontal_rule(line: &str) -> bool {
49    let trimmed = line.trim();
50    if trimmed.len() < 3 {
51        return false;
52    }
53
54    let mut marker = None;
55    let mut count = 0usize;
56
57    for character in trimmed.chars() {
58        if character.is_whitespace() {
59            continue;
60        }
61
62        if !matches!(character, '-' | '*' | '_') {
63            return false;
64        }
65
66        match marker {
67            Some(existing) if existing != character => return false,
68            Some(_) => {},
69            None => marker = Some(character),
70        }
71
72        count += 1;
73    }
74
75    count >= 3
76}
77
78/// Returns `true` when a line starts with a Markdown blockquote marker.
79pub fn is_blockquote(line: &str) -> bool {
80    line.trim_start().starts_with('>')
81}
82
83/// Returns `true` when a line starts with an unordered list marker.
84pub fn is_unordered_list_item(line: &str) -> bool {
85    unordered_list_item_content(line).is_some()
86}
87
88/// Returns `true` when a line starts with an ordered list marker.
89pub fn is_ordered_list_item(line: &str) -> bool {
90    ordered_list_item_content(line).is_some()
91}
92
93pub(crate) fn strip_up_to_three_leading_spaces(line: &str) -> &str {
94    let mut removed = 0usize;
95    let mut index = 0usize;
96
97    for (byte_index, character) in line.char_indices() {
98        if removed < 3 && character == ' ' {
99            removed += 1;
100            index = byte_index + character.len_utf8();
101        } else {
102            return &line[index..];
103        }
104    }
105
106    &line[index..]
107}
108
109pub(crate) fn unordered_list_item_content(line: &str) -> Option<&str> {
110    let trimmed = line.trim_start();
111    let mut characters = trimmed.chars();
112    let marker = characters.next()?;
113    if !matches!(marker, '-' | '*' | '+') {
114        return None;
115    }
116
117    let marker_length = marker.len_utf8();
118    let remainder = &trimmed[marker_length..];
119    remainder
120        .chars()
121        .next()
122        .filter(|character| character.is_whitespace())
123        .map(|character| &remainder[character.len_utf8()..])
124}
125
126pub(crate) fn ordered_list_item_content(line: &str) -> Option<&str> {
127    let trimmed = line.trim_start();
128    let digit_count = trimmed
129        .chars()
130        .take_while(|character| character.is_ascii_digit())
131        .count();
132    if digit_count == 0 {
133        return None;
134    }
135
136    let marker_start = trimmed
137        .char_indices()
138        .nth(digit_count)
139        .map_or(trimmed.len(), |(index, _)| index);
140    let marker = trimmed[marker_start..].chars().next()?;
141    if !matches!(marker, '.' | ')') {
142        return None;
143    }
144
145    let after_marker = &trimmed[marker_start + marker.len_utf8()..];
146    after_marker
147        .chars()
148        .next()
149        .filter(|character| character.is_whitespace())
150        .map(|character| &after_marker[character.len_utf8()..])
151}