atproto blogging
1// use std::{string::String, vec::Vec};
2// #[cfg(all(feature = "std", not(feature = "hashbrown")))]
3// use std::collections::HashMap;
4
5// #[cfg(feature = "hashbrown")]
6// use hashbrown::HashMap;
7//
8use std::collections::HashMap;
9//#[cfg(feature = "std")]
10use markdown_weaver_escape::IoWriter;
11use markdown_weaver_escape::{
12 FmtWriter, StrWrite, escape_href, escape_html, escape_html_body_text,
13};
14
15use markdown_weaver::{
16 Alignment, BlockQuoteKind, CodeBlockKind, CowStr, Event, Event::*, LinkType, Tag, TagEnd,
17};
18
19pub enum TableState {
20 Head,
21 Body,
22}
23
24struct HtmlWriter<'a, I, W> {
25 /// Iterator supplying events.
26 iter: I,
27
28 /// Writer to write to.
29 writer: W,
30
31 /// Whether or not the last write wrote a newline.
32 end_newline: bool,
33
34 /// Whether if inside a metadata block (text should not be written)
35 in_non_writing_block: bool,
36
37 table_state: TableState,
38 table_alignments: Vec<Alignment>,
39 table_cell_index: usize,
40 numbers: HashMap<CowStr<'a>, usize>,
41 /// Buffered paragraph opening tag (without closing `>`) for dir attribute emission
42 pending_paragraph_open: Option<String>,
43}
44
45impl<'a, I, W> HtmlWriter<'a, I, W>
46where
47 I: Iterator<Item = Event<'a>>,
48 W: StrWrite,
49{
50 fn new(iter: I, writer: W) -> Self {
51 Self {
52 iter,
53 writer,
54 end_newline: true,
55 in_non_writing_block: false,
56 table_state: TableState::Head,
57 table_alignments: vec![],
58 table_cell_index: 0,
59 numbers: HashMap::new(),
60 pending_paragraph_open: None,
61 }
62 }
63
64 /// Writes a new line.
65 #[inline]
66 fn write_newline(&mut self) -> Result<(), W::Error> {
67 self.end_newline = true;
68 self.writer.write_str("\n")
69 }
70
71 /// Writes a buffer, and tracks whether or not a newline was written.
72 #[inline]
73 fn write(&mut self, s: &str) -> Result<(), W::Error> {
74 self.writer.write_str(s)?;
75
76 if !s.is_empty() {
77 self.end_newline = s.ends_with('\n');
78 }
79 Ok(())
80 }
81
82 fn run(mut self) -> Result<(), W::Error> {
83 while let Some(event) = self.iter.next() {
84 match event {
85 Start(tag) => {
86 self.start_tag(tag)?;
87 }
88 End(tag) => {
89 self.end_tag(tag)?;
90 }
91 Text(text) => {
92 if !self.in_non_writing_block {
93 // Flush pending paragraph with dir attribute if needed
94 if let Some(opening) = self.pending_paragraph_open.take() {
95 if let Some(dir) = crate::utils::detect_text_direction(&text) {
96 self.write(&opening)?;
97 self.write(" dir=\"")?;
98 self.write(dir)?;
99 self.write("\">")?;
100 } else {
101 self.write(&opening)?;
102 self.write(">")?;
103 }
104 }
105 escape_html_body_text(&mut self.writer, &text)?;
106 self.end_newline = text.ends_with('\n');
107 }
108 }
109 Code(text) => {
110 self.write("<code>")?;
111 escape_html_body_text(&mut self.writer, &text)?;
112 self.write("</code>")?;
113 }
114 InlineMath(text) => match crate::math::render_math(&text, false) {
115 crate::math::MathResult::Success(mathml) => {
116 self.write(r#"<span class="math math-inline">"#)?;
117 self.write(&mathml)?;
118 self.write("</span>")?;
119 }
120 crate::math::MathResult::Error { html, .. } => {
121 self.write(&html)?;
122 }
123 },
124 DisplayMath(text) => match crate::math::render_math(&text, true) {
125 crate::math::MathResult::Success(mathml) => {
126 self.write(r#"<span class="math math-display">"#)?;
127 self.write(&mathml)?;
128 self.write("</span>")?;
129 }
130 crate::math::MathResult::Error { html, .. } => {
131 self.write(&html)?;
132 }
133 },
134 Html(html) | InlineHtml(html) => {
135 self.write(&html)?;
136 }
137 SoftBreak => {
138 self.write_newline()?;
139 }
140 HardBreak => {
141 self.write("<br />\n")?;
142 }
143 Rule => {
144 if self.end_newline {
145 self.write("<hr />\n")?;
146 } else {
147 self.write("\n<hr />\n")?;
148 }
149 }
150 FootnoteReference(name) => {
151 let len = self.numbers.len() + 1;
152 self.write("<sup class=\"footnote-reference\"><a href=\"#")?;
153 escape_html(&mut self.writer, &name)?;
154 self.write("\">")?;
155 let number = *self.numbers.entry(name).or_insert(len);
156 write!(&mut self.writer, "{}", number)?;
157 self.write("</a></sup>")?;
158 }
159 TaskListMarker(true) => {
160 self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\" aria-label=\"Completed task\"/>\n")?;
161 }
162 TaskListMarker(false) => {
163 self.write("<input disabled=\"\" type=\"checkbox\" aria-label=\"Incomplete task\"/>\n")?;
164 }
165 WeaverBlock(_text) => {}
166 }
167 }
168 Ok(())
169 }
170
171 /// Writes the start of an HTML tag.
172 fn start_tag(&mut self, tag: Tag<'a>) -> Result<(), W::Error> {
173 match tag {
174 Tag::HtmlBlock => Ok(()),
175 Tag::Paragraph(_) => {
176 // Buffer paragraph opening for dir attribute detection
177 let opening = if self.end_newline {
178 String::from("<p")
179 } else {
180 String::from("\n<p")
181 };
182 self.pending_paragraph_open = Some(opening);
183 Ok(())
184 }
185 Tag::Heading {
186 level,
187 id,
188 classes,
189 attrs,
190 } => {
191 if self.end_newline {
192 self.write("<")?;
193 } else {
194 self.write("\n<")?;
195 }
196 write!(&mut self.writer, "{}", level)?;
197 if let Some(id) = id {
198 self.write(" id=\"")?;
199 escape_html(&mut self.writer, &id)?;
200 self.write("\"")?;
201 }
202 let mut classes = classes.iter();
203 if let Some(class) = classes.next() {
204 self.write(" class=\"")?;
205 escape_html(&mut self.writer, class)?;
206 for class in classes {
207 self.write(" ")?;
208 escape_html(&mut self.writer, class)?;
209 }
210 self.write("\"")?;
211 }
212 for (attr, value) in attrs {
213 self.write(" ")?;
214 escape_html(&mut self.writer, &attr)?;
215 if let Some(val) = value {
216 self.write("=\"")?;
217 escape_html(&mut self.writer, &val)?;
218 self.write("\"")?;
219 } else {
220 self.write("=\"\"")?;
221 }
222 }
223 self.write(">")
224 }
225 Tag::Table(alignments) => {
226 self.table_alignments = alignments;
227 self.write("<table>")
228 }
229 Tag::TableHead => {
230 self.table_state = TableState::Head;
231 self.table_cell_index = 0;
232 self.write("<thead><tr>")
233 }
234 Tag::TableRow => {
235 self.table_cell_index = 0;
236 self.write("<tr>")
237 }
238 Tag::TableCell => {
239 match self.table_state {
240 TableState::Head => {
241 self.write("<th")?;
242 }
243 TableState::Body => {
244 self.write("<td")?;
245 }
246 }
247 match self.table_alignments.get(self.table_cell_index) {
248 Some(&Alignment::Left) => self.write(" style=\"text-align: left\">"),
249 Some(&Alignment::Center) => self.write(" style=\"text-align: center\">"),
250 Some(&Alignment::Right) => self.write(" style=\"text-align: right\">"),
251 _ => self.write(">"),
252 }
253 }
254 Tag::BlockQuote(kind) => {
255 let class_str = match kind {
256 None => "",
257 Some(kind) => match kind {
258 BlockQuoteKind::Note => " class=\"markdown-alert-note\"",
259 BlockQuoteKind::Tip => " class=\"markdown-alert-tip\"",
260 BlockQuoteKind::Important => " class=\"markdown-alert-important\"",
261 BlockQuoteKind::Warning => " class=\"markdown-alert-warning\"",
262 BlockQuoteKind::Caution => " class=\"markdown-alert-caution\"",
263 },
264 };
265 if self.end_newline {
266 self.write(&format!("<blockquote{}>\n", class_str))
267 } else {
268 self.write(&format!("\n<blockquote{}>\n", class_str))
269 }
270 }
271 Tag::CodeBlock(info) => {
272 if !self.end_newline {
273 self.write_newline()?;
274 }
275 match info {
276 CodeBlockKind::Fenced(info) => {
277 let lang = info.split(' ').next().unwrap();
278 if lang.is_empty() {
279 self.write("<pre><code>")
280 } else {
281 self.write("<pre><code class=\"language-")?;
282 escape_html(&mut self.writer, lang)?;
283 self.write("\">")
284 }
285 }
286 CodeBlockKind::Indented => self.write("<pre><code>"),
287 }
288 }
289 Tag::List(Some(1)) => {
290 if self.end_newline {
291 self.write("<ol>\n")
292 } else {
293 self.write("\n<ol>\n")
294 }
295 }
296 Tag::List(Some(start)) => {
297 if self.end_newline {
298 self.write("<ol start=\"")?;
299 } else {
300 self.write("\n<ol start=\"")?;
301 }
302 write!(&mut self.writer, "{}", start)?;
303 self.write("\">\n")
304 }
305 Tag::List(None) => {
306 if self.end_newline {
307 self.write("<ul>\n")
308 } else {
309 self.write("\n<ul>\n")
310 }
311 }
312 Tag::Item => {
313 if self.end_newline {
314 self.write("<li>")
315 } else {
316 self.write("\n<li>")
317 }
318 }
319 Tag::DefinitionList => {
320 if self.end_newline {
321 self.write("<dl>\n")
322 } else {
323 self.write("\n<dl>\n")
324 }
325 }
326 Tag::DefinitionListTitle => {
327 if self.end_newline {
328 self.write("<dt>")
329 } else {
330 self.write("\n<dt>")
331 }
332 }
333 Tag::DefinitionListDefinition => {
334 if self.end_newline {
335 self.write("<dd>")
336 } else {
337 self.write("\n<dd>")
338 }
339 }
340 Tag::Subscript => self.write("<sub>"),
341 Tag::Superscript => self.write("<sup>"),
342 Tag::Emphasis => self.write("<em>"),
343 Tag::Strong => self.write("<strong>"),
344 Tag::Strikethrough => self.write("<del>"),
345 Tag::Link {
346 link_type: LinkType::Email,
347 dest_url,
348 title,
349 id: _,
350 } => {
351 self.write("<a href=\"mailto:")?;
352 escape_href(&mut self.writer, &dest_url)?;
353 if !title.is_empty() {
354 self.write("\" title=\"")?;
355 escape_html(&mut self.writer, &title)?;
356 }
357 self.write("\">")
358 }
359 Tag::Link {
360 link_type: _,
361 dest_url,
362 title,
363 id: _,
364 } => {
365 self.write("<a href=\"")?;
366 escape_href(&mut self.writer, &dest_url)?;
367 if !title.is_empty() {
368 self.write("\" title=\"")?;
369 escape_html(&mut self.writer, &title)?;
370 }
371 self.write("\">")
372 }
373 Tag::Image {
374 link_type: _,
375 dest_url,
376 title,
377 id: _,
378 attrs,
379 } => {
380 self.write("<img src=\"")?;
381 escape_href(&mut self.writer, &dest_url)?;
382 if let Some(attrs) = attrs {
383 if !attrs.classes.is_empty() {
384 self.write("\" class=\"")?;
385 for class in &attrs.classes {
386 escape_html(&mut self.writer, class)?;
387 self.write(" ")?;
388 }
389 self.write("\" ")?;
390 } else {
391 self.write("\" ")?;
392 }
393 if !attrs.attrs.is_empty() {
394 for (attr, value) in &attrs.attrs {
395 escape_html(&mut self.writer, attr)?;
396 self.write("=\"")?;
397 escape_html(&mut self.writer, value)?;
398 self.write("\" ")?;
399 }
400 }
401 } else {
402 self.write("\" ")?;
403 }
404 self.write("alt=\"")?;
405 self.raw_text()?;
406 if !title.is_empty() {
407 self.write("\" title=\"")?;
408 escape_html(&mut self.writer, &title)?;
409 }
410 self.write("\" />")
411 }
412 Tag::Embed {
413 embed_type: _,
414 dest_url,
415 title,
416 id,
417 attrs,
418 } => {
419 // rewrite this to work correctly
420 self.write("<iframe src=\"")?;
421 escape_href(&mut self.writer, &dest_url)?;
422 self.write("\" title=\"")?;
423 escape_html(&mut self.writer, &title)?;
424 if !id.is_empty() {
425 self.write("\" id=\"")?;
426 escape_html(&mut self.writer, &id)?;
427 self.write("\"")?;
428 }
429 if let Some(attrs) = attrs {
430 self.write(" ")?;
431 if !attrs.classes.is_empty() {
432 self.write("class=\"")?;
433 for class in &attrs.classes {
434 escape_html(&mut self.writer, class)?;
435 self.write(" ")?;
436 }
437 self.write("\" ")?;
438 }
439 if !attrs.attrs.is_empty() {
440 for (attr, value) in &attrs.attrs {
441 escape_html(&mut self.writer, attr)?;
442 self.write("=\"")?;
443 escape_html(&mut self.writer, value)?;
444 self.write("\" ")?;
445 }
446 }
447 }
448 self.write("/>")
449 }
450 Tag::WeaverBlock(_, _attrs) => {
451 println!("Weaver block");
452 self.in_non_writing_block = true;
453 Ok(())
454 }
455 Tag::FootnoteDefinition(name) => {
456 if self.end_newline {
457 self.write("<div class=\"footnote-definition\" id=\"")?;
458 } else {
459 self.write("\n<div class=\"footnote-definition\" id=\"")?;
460 }
461 escape_html(&mut self.writer, &name)?;
462 self.write("\"><sup class=\"footnote-definition-label\">")?;
463 let len = self.numbers.len() + 1;
464 let number = *self.numbers.entry(name).or_insert(len);
465 write!(&mut self.writer, "{}", number)?;
466 self.write("</sup>")
467 }
468 Tag::MetadataBlock(_) => {
469 self.in_non_writing_block = true;
470 Ok(())
471 }
472 }
473 }
474
475 fn end_tag(&mut self, tag: TagEnd) -> Result<(), W::Error> {
476 match tag {
477 TagEnd::HtmlBlock => {}
478 TagEnd::Paragraph(_) => {
479 // Flush any pending paragraph open (for empty paragraphs)
480 if let Some(opening) = self.pending_paragraph_open.take() {
481 self.write(&opening)?;
482 self.write(">")?;
483 }
484 self.write("</p>\n")?;
485 }
486 TagEnd::Heading(level) => {
487 self.write("</")?;
488 write!(&mut self.writer, "{}", level)?;
489 self.write(">\n")?;
490 }
491 TagEnd::Table => {
492 self.write("</tbody></table>\n")?;
493 }
494 TagEnd::TableHead => {
495 self.write("</tr></thead><tbody>\n")?;
496 self.table_state = TableState::Body;
497 }
498 TagEnd::TableRow => {
499 self.write("</tr>\n")?;
500 }
501 TagEnd::TableCell => {
502 match self.table_state {
503 TableState::Head => {
504 self.write("</th>")?;
505 }
506 TableState::Body => {
507 self.write("</td>")?;
508 }
509 }
510 self.table_cell_index += 1;
511 }
512 TagEnd::BlockQuote(_) => {
513 self.write("</blockquote>\n")?;
514 }
515 TagEnd::CodeBlock => {
516 self.write("</code></pre>\n")?;
517 }
518 TagEnd::List(true) => {
519 self.write("</ol>\n")?;
520 }
521 TagEnd::List(false) => {
522 self.write("</ul>\n")?;
523 }
524 TagEnd::Item => {
525 self.write("</li>\n")?;
526 }
527 TagEnd::DefinitionList => {
528 self.write("</dl>\n")?;
529 }
530 TagEnd::DefinitionListTitle => {
531 self.write("</dt>\n")?;
532 }
533 TagEnd::DefinitionListDefinition => {
534 self.write("</dd>\n")?;
535 }
536 TagEnd::Emphasis => {
537 self.write("</em>")?;
538 }
539 TagEnd::Superscript => {
540 self.write("</sup>")?;
541 }
542 TagEnd::Subscript => {
543 self.write("</sub>")?;
544 }
545 TagEnd::Strong => {
546 self.write("</strong>")?;
547 }
548 TagEnd::Strikethrough => {
549 self.write("</del>")?;
550 }
551 TagEnd::Link => {
552 self.write("</a>")?;
553 }
554 TagEnd::Image => (), // shouldn't happen, handled in start
555 TagEnd::Embed => (), // shouldn't happen, handled in start
556 TagEnd::WeaverBlock(_) => {
557 self.in_non_writing_block = false;
558 }
559 TagEnd::FootnoteDefinition => {
560 self.write("</div>\n")?;
561 }
562 TagEnd::MetadataBlock(_) => {
563 self.in_non_writing_block = false;
564 }
565 }
566 Ok(())
567 }
568
569 // run raw text, consuming end tag
570 fn raw_text(&mut self) -> Result<(), W::Error> {
571 let mut nest = 0;
572 while let Some(event) = self.iter.next() {
573 match event {
574 Start(_) => nest += 1,
575 End(_) => {
576 if nest == 0 {
577 break;
578 }
579 nest -= 1;
580 }
581 Html(_) => {}
582 InlineHtml(text) | Code(text) | Text(text) => {
583 // Don't use escape_html_body_text here.
584 // The output of this function is used in the `alt` attribute.
585 escape_html(&mut self.writer, &text)?;
586 self.end_newline = text.ends_with('\n');
587 }
588 InlineMath(text) => {
589 self.write("$")?;
590 escape_html(&mut self.writer, &text)?;
591 self.write("$")?;
592 }
593 DisplayMath(text) => {
594 self.write("$$")?;
595 escape_html(&mut self.writer, &text)?;
596 self.write("$$")?;
597 }
598 SoftBreak | HardBreak | Rule => {
599 self.write(" ")?;
600 }
601 FootnoteReference(name) => {
602 let len = self.numbers.len() + 1;
603 let number = *self.numbers.entry(name).or_insert(len);
604 write!(&mut self.writer, "[{}]", number)?;
605 }
606 TaskListMarker(true) => self.write("[x]")?,
607 TaskListMarker(false) => self.write("[ ]")?,
608 WeaverBlock(_) => {
609 println!("Weaver block internal");
610 }
611 }
612 }
613 Ok(())
614 }
615}
616
617/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
618/// push it to a `String`.
619pub fn push_html<'a, I>(s: &mut String, iter: I)
620where
621 I: Iterator<Item = Event<'a>>,
622{
623 write_html_fmt(s, iter).unwrap()
624}
625
626/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
627/// write it out to an I/O stream.
628///
629/// **Note**: using this function with an unbuffered writer like a file or socket
630/// will result in poor performance. Wrap these in a
631/// [`BufWriter`](https://doc.rust-lang.org/std/io/struct.BufWriter.html) to
632/// prevent unnecessary slowdowns.
633
634//#[cfg(feature = "std")]
635pub fn write_html_io<'a, I, W>(writer: W, iter: I) -> std::io::Result<()>
636where
637 I: Iterator<Item = Event<'a>>,
638 W: std::io::Write,
639{
640 HtmlWriter::new(iter, IoWriter(writer)).run()
641}
642
643/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
644/// write it into Unicode-accepting buffer or stream.
645
646pub fn write_html_fmt<'a, I, W>(writer: W, iter: I) -> core::fmt::Result
647where
648 I: Iterator<Item = Event<'a>>,
649 W: core::fmt::Write,
650{
651 HtmlWriter::new(iter, FmtWriter(writer)).run()
652}