weimo_wdf/adapters/
markdown.rs1use crate::{
2 diagnostic::{Diagnostic, Diagnostics},
3 model::*,
4 validate::validate_and_canonicalize,
5};
6use pulldown_cmark::{Event, Parser, Tag, TagEnd};
7use serde_json::json;
8
9pub fn from_markdown(input: &str) -> Result<WdfDocument, Diagnostics> {
25 let events: Vec<Event<'_>> = Parser::new(input).collect();
26 let mut cursor = Cursor {
27 events,
28 index: 0,
29 diagnostics: Vec::new(),
30 };
31 let blocks = cursor.parse_blocks(None);
32 if !cursor.diagnostics.is_empty() {
33 return Err(cursor.diagnostics);
34 }
35 let value = json!({"schema": WDF_SCHEMA, "version": WDF_VERSION, "root": {"type":"doc", "content": blocks}});
36 validate_and_canonicalize(&value)
37}
38
39struct Cursor<'a> {
40 events: Vec<Event<'a>>,
41 index: usize,
42 diagnostics: Diagnostics,
43}
44
45impl Cursor<'_> {
46 fn parse_blocks(&mut self, terminator: Option<TagEnd>) -> Vec<WdfBlock> {
47 let mut blocks = Vec::new();
48 while self.index < self.events.len() {
49 if matches!(terminator, Some(end) if matches!((&self.events[self.index], end), (Event::End(actual), expected) if *actual == expected))
50 {
51 self.index += 1;
52 break;
53 }
54 let event = self.events[self.index].clone();
55 match event {
56 Event::Start(Tag::Paragraph) => {
57 self.index += 1;
58 blocks.push(WdfBlock::Paragraph {
59 content: self.parse_inline(TagEnd::Paragraph, false),
60 });
61 }
62 Event::Start(Tag::Heading { level, .. }) => {
63 self.index += 1;
64 blocks.push(WdfBlock::Heading {
65 attrs: HeadingAttrs { level: level as u8 },
66 content: self.parse_inline(TagEnd::Heading(level), false),
67 });
68 }
69 Event::Start(Tag::BlockQuote(_)) => {
70 self.index += 1;
71 let content = self.parse_blocks(Some(TagEnd::BlockQuote(None)));
72 let mut paragraphs = Vec::new();
73 for block in content {
74 match block {
75 WdfBlock::Paragraph { content } => {
76 paragraphs.extend(split_paragraph_lines(content))
77 }
78 _ => self.unsupported("blockquote child"),
79 }
80 }
81 blocks.push(WdfBlock::Blockquote {
82 content: paragraphs,
83 });
84 }
85 Event::Start(Tag::List(start)) => {
86 self.index += 1;
87 let mut items = Vec::new();
88 while self.index < self.events.len() {
89 if matches!(self.events[self.index], Event::End(TagEnd::List(_))) {
90 self.index += 1;
91 break;
92 } else if matches!(self.events[self.index], Event::Start(Tag::Item)) {
93 self.index += 1;
94 if !matches!(
97 self.events.get(self.index),
98 Some(Event::Start(Tag::Paragraph))
99 ) {
100 items.push(list_item(self.parse_inline(TagEnd::Item, false)));
101 continue;
102 }
103 let item_blocks = self.parse_blocks(Some(TagEnd::Item));
104 if item_blocks.len() == 1 {
105 if let WdfBlock::Paragraph { content } =
106 item_blocks.into_iter().next().unwrap()
107 {
108 items.push(list_item(content));
109 } else {
110 self.unsupported("list item");
111 }
112 } else {
113 self.unsupported("list item content");
114 }
115 } else {
116 self.unsupported("list content");
117 self.index += 1;
118 }
119 }
120 if start.is_some() {
121 blocks.push(WdfBlock::OrderedList {
122 attrs: Some(OrderedListAttrs {
123 start: start.unwrap_or(1) as u32,
124 }),
125 content: items,
126 });
127 } else {
128 blocks.push(WdfBlock::BulletList { content: items });
129 }
130 }
131 Event::Text(text) => {
132 self.index += 1;
133 blocks.push(WdfBlock::Paragraph {
134 content: vec![WdfText::plain(text.to_string())],
135 });
136 }
137 Event::End(_) => {
138 self.index += 1;
139 }
140 _ => {
141 self.unsupported("block");
142 self.index += 1;
143 }
144 }
145 }
146 blocks
147 }
148
149 fn parse_inline(&mut self, terminator: TagEnd, bold: bool) -> Vec<WdfText> {
150 let mut result = Vec::new();
151 while self.index < self.events.len() {
152 let event = self.events[self.index].clone();
153 if matches!(event, Event::End(end) if end == terminator) {
154 self.index += 1;
155 break;
156 }
157 match event {
158 Event::Text(text) => {
159 self.index += 1;
160 result.push(if bold {
161 WdfText::bold(text.to_string())
162 } else {
163 WdfText::plain(text.to_string())
164 });
165 }
166 Event::Start(Tag::Strong) => {
167 self.index += 1;
168 result.extend(self.parse_inline(TagEnd::Strong, true));
169 }
170 Event::SoftBreak | Event::HardBreak => {
171 self.index += 1;
172 result.push(if bold {
173 WdfText::bold("\n")
174 } else {
175 WdfText::plain("\n")
176 });
177 }
178 Event::Code(code) => {
179 self.unsupported("code");
180 self.index += 1;
181 let _ = code;
182 }
183 Event::End(_) => {
184 self.index += 1;
185 }
186 _ => {
187 self.unsupported("inline");
188 self.index += 1;
189 }
190 }
191 }
192 result
193 }
194
195 fn unsupported(&mut self, kind: &str) {
196 self.diagnostics.push(Diagnostic::error(
197 "markdown",
198 &format!("markdown.unsupported_{kind}"),
199 format!("Markdown {kind} is outside the supported subset"),
200 ));
201 }
202}
203
204fn split_paragraph_lines(content: Vec<WdfText>) -> Vec<WdfParagraph> {
205 let mut paragraphs: Vec<Vec<WdfText>> = vec![Vec::new()];
206 for text in content {
207 let parts: Vec<&str> = text.text.split('\n').collect();
208 for (index, part) in parts.iter().enumerate() {
209 if !part.is_empty() {
210 paragraphs
211 .last_mut()
212 .expect("paragraph")
213 .push(if text.is_bold() {
214 WdfText::bold(*part)
215 } else {
216 WdfText::plain(*part)
217 });
218 }
219 if index + 1 < parts.len() {
220 paragraphs.push(Vec::new());
221 }
222 }
223 }
224 paragraphs
225 .into_iter()
226 .filter(|content| !content.is_empty())
227 .map(paragraph)
228 .collect()
229}
230
231pub fn to_markdown(document: &WdfDocument) -> String {
238 let mut output = document
239 .root
240 .content
241 .iter()
242 .map(block_markdown)
243 .collect::<Vec<_>>()
244 .join("\n\n");
245 output.push('\n');
246 output
247}
248
249fn inline_markdown(content: &[WdfText]) -> String {
250 content
251 .iter()
252 .map(|text| {
253 if text.is_bold() {
254 format!("**{}**", text.text.replace("**", "\\**"))
255 } else {
256 text.text.clone()
257 }
258 })
259 .collect()
260}
261fn block_markdown(block: &WdfBlock) -> String {
262 match block {
263 WdfBlock::Paragraph { content } => inline_markdown(content),
264 WdfBlock::Heading { attrs, content } => format!(
265 "{} {}",
266 "#".repeat(attrs.level as usize),
267 inline_markdown(content)
268 ),
269 WdfBlock::Blockquote { content } => content
270 .iter()
271 .map(|paragraph| {
272 let body = inline_markdown(¶graph.content);
273 body.split('\n')
274 .map(|line| {
275 if line.is_empty() {
276 ">".to_string()
277 } else {
278 format!("> {line}")
279 }
280 })
281 .collect::<Vec<_>>()
282 .join("\n")
283 })
284 .collect::<Vec<_>>()
285 .join("\n"),
286 WdfBlock::BulletList { content } => content
287 .iter()
288 .map(|item| format!("- {}", inline_markdown(&item.content[0].content)))
289 .collect::<Vec<_>>()
290 .join("\n"),
291 WdfBlock::OrderedList { attrs, content } => content
292 .iter()
293 .enumerate()
294 .map(|(index, item)| {
295 format!(
296 "{}. {}",
297 attrs.as_ref().map_or(1, |a| a.start) + index as u32,
298 inline_markdown(&item.content[0].content)
299 )
300 })
301 .collect::<Vec<_>>()
302 .join("\n"),
303 }
304}