1use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd, html};
2
3pub(crate) fn extract_local_refs(md: &str) -> Vec<String> {
4 let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH;
5 let mut refs = Vec::new();
6 for event in Parser::new_ext(md, opts) {
7 let url: Option<pulldown_cmark::CowStr> = match event {
8 Event::Start(Tag::Image { dest_url, .. }) => Some(dest_url),
9 Event::Start(Tag::Link { dest_url, .. }) => Some(dest_url),
10 Event::End(TagEnd::Image | TagEnd::Link) => None,
11 _ => None,
12 };
13 if let Some(url) = url {
14 let s = url.as_ref();
15 if !s.contains("://") && !s.starts_with('#') && !s.is_empty() {
16 refs.push(s.to_owned());
17 }
18 }
19 }
20 refs
21}
22
23pub(crate) fn render_markdown_html(md: &str) -> String {
24 let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH;
25 let parser = Parser::new_ext(md, opts);
26 let mut buf = String::new();
27 html::push_html(&mut buf, parser);
28 buf
29}
30
31pub(crate) fn render_markdown_gemtext(md: &str) -> String {
32 let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH;
33 let parser = Parser::new_ext(md, opts);
34
35 let mut out: Vec<String> = Vec::new();
36 let mut buf = String::new();
37 let mut links: Vec<(String, String)> = Vec::new();
38 let mut last_blank = false;
39
40 enum Ctx {
41 Paragraph,
42 BlockQuote,
43 ListItem,
44 Heading(u8),
45 }
46 let mut stack: Vec<Ctx> = Vec::new();
47
48 let mut in_link = false;
49 let mut link_url = String::new();
50 let mut link_text = String::new();
51
52 let mut in_code = false;
53 let mut code_kind = String::new();
54 let mut code_body = String::new();
55
56 fn flush_line(
57 out: &mut Vec<String>,
58 buf: &mut String,
59 links: &mut Vec<(String, String)>,
60 stack: &[Ctx],
61 _last_blank: &mut bool,
62 ) {
63 let text = buf.trim();
64 if text.is_empty() && links.is_empty() {
65 return;
66 }
67
68 if let Some(Ctx::Heading(lvl)) = stack.last() {
69 let prefix = match lvl {
70 1 => "#",
71 2 => "##",
72 _ => "###",
73 };
74 if !text.is_empty() {
75 out.push(format!("{prefix} {text}"));
76 }
77 for (url, t) in links.drain(..) {
78 out.push(format!("=> {url} {t}"));
79 }
80 *_last_blank = false;
81 } else if let Some(Ctx::BlockQuote) = stack.last() {
82 if !text.is_empty() {
83 for line in text.lines() {
84 out.push(format!("> {line}"));
85 }
86 }
87 for (url, t) in links.drain(..) {
88 out.push(format!("=> {url} {t}"));
89 }
90 *_last_blank = false;
91 } else if let Some(Ctx::ListItem) = stack.last() {
92 if !text.is_empty() {
93 out.push(format!("* {text}"));
94 }
95 for (url, t) in links.drain(..) {
96 out.push(format!("=> {url} {t}"));
97 }
98 *_last_blank = false;
99 } else {
100 if !text.is_empty() {
101 out.push(text.to_string());
102 }
103 for (url, t) in links.drain(..) {
104 out.push(format!("=> {url} {t}"));
105 }
106 *_last_blank = false;
107 }
108 buf.clear();
109 }
110
111 for event in parser {
112 match event {
113 Event::Start(tag) => match tag {
114 Tag::Heading { level, .. } => {
115 flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
116 let lvl = level as u8;
117 stack.push(Ctx::Heading(if lvl > 3 { 3 } else { lvl }));
118 }
119 Tag::Paragraph => {
120 stack.push(Ctx::Paragraph);
121 }
122 Tag::BlockQuote(_) => {
123 flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
124 stack.push(Ctx::BlockQuote);
125 }
126 Tag::CodeBlock(kind) => {
127 flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
128 in_code = true;
129 code_kind = match kind {
130 CodeBlockKind::Fenced(info) => info.to_string(),
131 CodeBlockKind::Indented => String::new(),
132 };
133 code_body.clear();
134 }
135 Tag::List { .. } => {}
136 Tag::Item => {
137 flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
138 stack.push(Ctx::ListItem);
139 }
140 Tag::Link { dest_url, .. } => {
141 in_link = true;
142 link_url = dest_url.to_string();
143 link_text.clear();
144 }
145 Tag::Image { dest_url, .. } => {
146 in_link = true;
147 link_url = dest_url.to_string();
148 link_text.clear();
149 }
150 _ => {}
151 },
152 Event::End(tag) => match tag {
153 TagEnd::Heading(_) => {
154 flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
155 stack.pop();
156 }
157 TagEnd::Paragraph => {
158 flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
159 if !matches!(stack.last(), Some(Ctx::BlockQuote | Ctx::ListItem)) {
160 out.push(String::new());
161 last_blank = true;
162 }
163 stack.pop();
164 }
165 TagEnd::BlockQuote(_) => {
166 flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
167 out.push(String::new());
168 last_blank = true;
169 stack.pop();
170 }
171 TagEnd::CodeBlock => {
172 if !code_body.is_empty() {
173 out.push(format!("```{}", code_kind));
174 for line in code_body.trim().lines() {
175 out.push(line.to_string());
176 }
177 out.push("```".to_string());
178 out.push(String::new());
179 last_blank = true;
180 }
181 in_code = false;
182 code_kind.clear();
183 code_body.clear();
184 }
185 TagEnd::List(_) => {
186 out.push(String::new());
187 last_blank = true;
188 }
189 TagEnd::Item => {
190 flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
191 stack.pop();
192 }
193 TagEnd::Link => {
194 let t = link_text.trim().to_string();
195 if in_link && !link_url.is_empty() && !t.is_empty() {
196 links.push((link_url.clone(), t));
197 }
198 in_link = false;
199 link_url.clear();
200 link_text.clear();
201 }
202 TagEnd::Image => {
203 let t = if link_text.trim().is_empty() {
204 "image".to_string()
205 } else {
206 link_text.trim().to_string()
207 };
208 if in_link {
209 out.push(format!("=> {} {}", link_url, t));
210 }
211 in_link = false;
212 link_url.clear();
213 link_text.clear();
214 }
215 _ => {}
216 },
217 Event::Text(text) => {
218 if in_code {
219 code_body.push_str(&text);
220 } else if in_link {
221 link_text.push_str(&text);
222 } else {
223 buf.push_str(&text);
224 }
225 }
226 Event::Code(text) => {
227 if in_link {
228 link_text.push_str(&text);
229 } else {
230 buf.push_str(&text);
231 }
232 }
233 Event::SoftBreak | Event::HardBreak => {
234 if in_code {
235 code_body.push('\n');
236 } else {
237 buf.push(' ');
238 }
239 }
240 _ => {}
241 }
242 }
243
244 flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
245
246 while out.last().is_some_and(|l| l.is_empty()) {
247 out.pop();
248 }
249
250 out.join("\n") + "\n"
251}