1pub mod adf;
71
72use adf::{Mark, Node, mark::LinkAttrs, node::CodeBlockAttrs};
73use markdown::{
74 mdast::{self, Code, InlineCode, Link, Paragraph, Root, Text},
75 message::Message,
76};
77use std::fmt::Display;
78
79pub use markdown::ParseOptions;
80
81#[derive(Debug)]
82pub enum Error {
83 Markdown(Message),
84}
85
86impl Display for Error {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 Error::Markdown(message) => write!(f, "{message}"),
90 }
91 }
92}
93
94impl std::error::Error for Error {}
95
96impl From<Message> for Error {
97 fn from(value: Message) -> Self {
98 Self::Markdown(value)
99 }
100}
101
102pub fn from_str<T: AsRef<str>>(markdown: T) -> Result<Node, Error> {
108 Ok(
109 markdown::to_mdast(markdown.as_ref(), &ParseOptions::default())?
110 .to_adf(),
111 )
112}
113
114pub trait ToAdf {
115 fn to_adf(&self) -> Node;
117}
118
119impl ToAdf for mdast::Node {
120 fn to_adf(&self) -> Node {
121 to_adf(self)
122 }
123}
124
125fn to_adf(node: &mdast::Node) -> Node {
126 match node {
127 mdast::Node::Root(Root {
128 children,
129 ..
130 }) => Node::Doc {
131 content: children.iter().map(to_adf).collect(),
132 version: 1,
133 },
134 mdast::Node::Blockquote(blockquote) => todo!(),
135 mdast::Node::Break(_) => todo!(),
136 mdast::Node::Code(Code {
137 value,
138 lang,
139 ..
140 }) => Node::codeblock()
141 .and_attrs(
142 lang.as_ref()
143 .map(|l| CodeBlockAttrs::builder().language(l).build()),
144 )
145 .content_entry(Node::text().text(value).build())
146 .build(),
147 mdast::Node::Definition(definition) => todo!(),
148 mdast::Node::Delete(delete) => todo!(),
149 mdast::Node::Emphasis(emphasis) => todo!(),
150 mdast::Node::FootnoteDefinition(footnote_definition) => todo!(),
151 mdast::Node::FootnoteReference(footnote_reference) => todo!(),
152 mdast::Node::Heading(heading) => todo!(),
153 mdast::Node::Html(html) => todo!(),
154 mdast::Node::Image(image) => todo!(),
155 mdast::Node::ImageReference(image_reference) => todo!(),
156 mdast::Node::InlineCode(InlineCode {
157 value,
158 ..
159 }) => Node::Text {
160 text: value.to_string(),
161 marks: vec![Mark::Code],
162 },
163 mdast::Node::InlineMath(inline_math) => todo!(),
164 mdast::Node::Link(Link {
165 url,
166 children,
167 title,
168 ..
169 }) => {
170 let Some(mdast::Node::Text(Text {
171 value,
172 ..
173 })) = children.first()
174 else {
175 panic!("missing text on link");
177 };
178
179 Node::Text {
180 text: value.to_string(),
181 marks: vec![Mark::Link {
182 attrs: LinkAttrs::builder()
183 .href(url)
184 .and_title(title.as_ref())
185 .build(),
186 }],
187 }
188 },
189 mdast::Node::LinkReference(link_reference) => todo!(),
190 mdast::Node::List(list) => todo!(),
191 mdast::Node::ListItem(list_item) => todo!(),
192 mdast::Node::Math(math) => todo!(),
193 mdast::Node::MdxFlowExpression(mdx_flow_expression) => todo!(),
194 mdast::Node::MdxJsxFlowElement(mdx_jsx_flow_element) => todo!(),
195 mdast::Node::MdxJsxTextElement(mdx_jsx_text_element) => todo!(),
196 mdast::Node::MdxTextExpression(mdx_text_expression) => todo!(),
197 mdast::Node::MdxjsEsm(mdxjs_esm) => todo!(),
198 mdast::Node::Paragraph(Paragraph {
199 children,
200 ..
201 }) => Node::paragraph()
202 .content(children.iter().map(to_adf).collect())
203 .build(),
204 mdast::Node::Strong(strong) => todo!(),
205 mdast::Node::Table(table) => todo!(),
206 mdast::Node::TableCell(table_cell) => todo!(),
207 mdast::Node::TableRow(table_row) => todo!(),
208 mdast::Node::Text(Text {
209 value,
210 ..
211 }) => Node::text().text(value).build(),
212 mdast::Node::ThematicBreak(thematic_break) => todo!(),
213 mdast::Node::Toml(toml) => todo!(),
214 mdast::Node::Yaml(yaml) => todo!(),
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use markdown::mdast::Node;
222 use serde_json::json;
223
224 mod to_adf {
225 use super::*;
226 use markdown::{ParseOptions, mdast::Root};
227 use pretty_assertions::assert_eq;
228
229 #[test]
230 fn test_empty_root() {
231 assert_eq!(
232 serde_json::to_value(
233 Node::Root(Root {
234 children: vec![],
235 position: None,
236 })
237 .to_adf()
238 )
239 .unwrap(),
240 json!({
241 "content": [],
242 "type": "doc",
243 "version": 1,
244 }),
245 );
246 }
247
248 #[test]
249 fn test_paragraph() {
250 let node = markdown::to_mdast(
251 "This is a paragraph.",
252 &ParseOptions::default(),
253 )
254 .unwrap();
255 assert_eq!(
256 serde_json::to_value(node.to_adf()).unwrap(),
257 json!({
258 "content": [{
259 "content": [{
260 "text": "This is a paragraph.",
261 "type": "text",
262 }],
263 "type": "paragraph",
264 }],
265 "type": "doc",
266 "version": 1,
267 }),
268 "{node:#?}",
269 );
270
271 let node = serde_json::to_value(
272 markdown::to_mdast("", &ParseOptions::default())
273 .unwrap()
274 .to_adf(),
275 )
276 .unwrap();
277 assert_eq!(
278 node,
279 json!({
280 "content": [],
281 "type": "doc",
282 "version": 1,
283 }),
284 "{:#?}",
285 node,
286 );
287 }
288
289 #[test]
290 fn test_multiline_paragraph() {
291 let node = serde_json::to_value(
292 markdown::to_mdast(
293 "This is a\nmultiline paragraph.",
294 &ParseOptions::default(),
295 )
296 .unwrap()
297 .to_adf(),
298 )
299 .unwrap();
300 assert_eq!(
301 node,
302 json!({
303 "content": [{
304 "content": [{
305 "text": "This is a\nmultiline paragraph.",
306 "type": "text",
307 }],
308 "type": "paragraph",
309 }],
310 "type": "doc",
311 "version": 1,
312 }),
313 "{node:#?}",
314 );
315 }
316
317 #[test]
318 fn test_multi_paragraph() {
319 let node = serde_json::to_value(
320 markdown::to_mdast(
321 "This is a paragraph.\n\nAnd another one.",
322 &ParseOptions::default(),
323 )
324 .unwrap()
325 .to_adf(),
326 )
327 .unwrap();
328 assert_eq!(
329 node,
330 json!({
331 "content": [{
332 "content": [{
333 "text": "This is a paragraph.",
334 "type": "text",
335 }],
336 "type": "paragraph",
337 },{
338 "content": [{
339 "text": "And another one.",
340 "type": "text",
341 }],
342 "type": "paragraph",
343 }],
344 "type": "doc",
345 "version": 1,
346 }),
347 "{node:#?}",
348 );
349 }
350
351 #[test]
352 fn test_link() {
353 let node = serde_json::to_value(
354 markdown::to_mdast(
355 "This is a paragraph [with](https://example.com).",
356 &ParseOptions::default(),
357 )
358 .unwrap()
359 .to_adf(),
360 )
361 .unwrap();
362 assert_eq!(
363 node,
364 json!({
365 "content": [{
366 "content": [{
367 "text": "This is a paragraph ",
368 "type": "text",
369 },{
370 "text": "with",
371 "type": "text",
372 "marks": [{
373 "attrs": {
374 "href": "https://example.com",
375 },
376 "type": "link",
377 }],
378 },{
379 "text": ".",
380 "type": "text",
381 }],
382 "type": "paragraph",
383 }],
384 "type": "doc",
385 "version": 1,
386 }),
387 "{node:#?}",
388 );
389 }
390
391 #[test]
392 fn test_link_with_title() {
393 let node = serde_json::to_value(markdown::to_mdast(
394 r#"This is a paragraph [with](https://example.com "my title")."#,
395 &ParseOptions::default(),
396 )
397 .unwrap().to_adf()).unwrap();
398 assert_eq!(
399 node,
400 json!({
401 "content": [{
402 "content": [{
403 "text": "This is a paragraph ",
404 "type": "text",
405 },{
406 "text": "with",
407 "type": "text",
408 "marks": [{
409 "attrs": {
410 "href": "https://example.com",
411 "title": "my title",
412 },
413 "type": "link",
414 }],
415 },{
416 "text": ".",
417 "type": "text",
418 }],
419 "type": "paragraph",
420 }],
421 "type": "doc",
422 "version": 1,
423 }),
424 "{node:#?}",
425 );
426 }
427
428 #[test]
429 fn test_inline_code() {
430 let node = serde_json::to_value(
431 markdown::to_mdast(
432 "inline `codehighlight` block",
433 &ParseOptions::default(),
434 )
435 .unwrap()
436 .to_adf(),
437 )
438 .unwrap();
439 assert_eq!(
440 node,
441 json!({
442 "content": [{
443 "content": [{
444 "text": "inline ",
445 "type": "text",
446 },{
447 "type": "text",
448 "text": "codehighlight",
449 "marks": [{
450 "type": "code",
451 }],
452 },{
453 "text": " block",
454 "type": "text",
455 }],
456 "type": "paragraph",
457 }],
458 "type": "doc",
459 "version": 1,
460 }),
461 "{node:#?}",
462 );
463 }
464 }
465}