Skip to content

Commit 6a85b89

Browse files
authored
feat: Improve mdbook preprocessor formatting (#290)
* feat: add global instances to LAD format * link to test data in readme * feat: create initial mdbook `LAD` backend * add debug logging for integration tests * feat: intial work on the markdown plugin * improve markdown
1 parent e0d0498 commit 6a85b89

File tree

2 files changed

+265
-84
lines changed

2 files changed

+265
-84
lines changed

crates/lad_backends/mdbook_lad_preprocessor/src/markdown.rs

Lines changed: 218 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::borrow::Cow;
2+
13
/// Escapes Markdown reserved characters in the given text.
24
fn escape_markdown(text: &str) -> String {
35
// Characters that should be escaped in markdown
@@ -25,8 +27,12 @@ pub enum Markdown {
2527
level: u8,
2628
text: String,
2729
},
28-
Paragraph(String),
29-
InlineCode(String),
30+
Paragraph {
31+
text: String,
32+
bold: bool,
33+
italic: bool,
34+
code: bool,
35+
},
3036
CodeBlock {
3137
language: Option<String>,
3238
code: String,
@@ -52,6 +58,54 @@ pub enum Markdown {
5258
},
5359
}
5460

61+
#[allow(dead_code)]
62+
impl Markdown {
63+
pub fn new_paragraph(text: impl Into<String>) -> Self {
64+
Markdown::Paragraph {
65+
text: text.into(),
66+
bold: false,
67+
italic: false,
68+
code: false,
69+
}
70+
}
71+
72+
pub fn bold(self) -> Self {
73+
match self {
74+
Markdown::Paragraph { text, .. } => Markdown::Paragraph {
75+
text,
76+
bold: true,
77+
italic: false,
78+
code: false,
79+
},
80+
_ => self,
81+
}
82+
}
83+
84+
pub fn italic(self) -> Self {
85+
match self {
86+
Markdown::Paragraph { text, .. } => Markdown::Paragraph {
87+
text,
88+
bold: false,
89+
italic: true,
90+
code: false,
91+
},
92+
_ => self,
93+
}
94+
}
95+
96+
pub fn code(self) -> Self {
97+
match self {
98+
Markdown::Paragraph { text, .. } => Markdown::Paragraph {
99+
text,
100+
bold: false,
101+
italic: false,
102+
code: true,
103+
},
104+
_ => self,
105+
}
106+
}
107+
}
108+
55109
impl IntoMarkdown for Markdown {
56110
fn to_markdown(&self, builder: &mut MarkdownBuilder) {
57111
match self {
@@ -62,28 +116,54 @@ impl IntoMarkdown for Markdown {
62116
// Escape the text for Markdown
63117
builder.append(&format!("{} {}", hashes, escape_markdown(text)));
64118
}
65-
Markdown::Paragraph(text) => {
66-
builder.append(&escape_markdown(text));
119+
Markdown::Paragraph {
120+
text,
121+
bold,
122+
italic,
123+
code,
124+
} => {
125+
if *bold {
126+
builder.append("**");
127+
}
128+
if *italic {
129+
builder.append("_");
130+
}
131+
if *code {
132+
builder.append("`");
133+
}
134+
135+
let escaped = if *code {
136+
text.clone()
137+
} else {
138+
escape_markdown(text)
139+
};
140+
141+
builder.append(&escaped);
142+
143+
if *code {
144+
builder.append("`");
145+
}
146+
if *italic {
147+
builder.append("_");
148+
}
149+
if *bold {
150+
builder.append("**");
151+
}
67152
}
68153
Markdown::CodeBlock { language, code } => {
69154
// Do not escape code blocks
70155
let lang = language.as_deref().unwrap_or("");
71156
builder.append(&format!("```{}\n{}\n```", lang, code));
72157
}
73-
Markdown::InlineCode(code) => {
74-
// Do not escape inline code
75-
builder.append(&format!("`{}`", code));
76-
}
77158
Markdown::List { ordered, items } => {
78159
let list_output = items
79160
.iter()
80161
.enumerate()
81162
.map(|(i, item)| {
82-
let escaped_item = escape_markdown(item);
83163
if *ordered {
84-
format!("{}. {}", i + 1, escaped_item)
164+
format!("{}. {}", i + 1, item)
85165
} else {
86-
format!("- {}", escaped_item)
166+
format!("- {}", item)
87167
}
88168
})
89169
.collect::<Vec<String>>()
@@ -149,6 +229,56 @@ impl IntoMarkdown for Markdown {
149229
}
150230
}
151231

232+
impl IntoMarkdown for &str {
233+
fn to_markdown(&self, builder: &mut MarkdownBuilder) {
234+
builder.append(&escape_markdown(self))
235+
}
236+
}
237+
238+
impl IntoMarkdown for String {
239+
fn to_markdown(&self, builder: &mut MarkdownBuilder) {
240+
builder.append(&escape_markdown(self.as_ref()))
241+
}
242+
}
243+
244+
impl IntoMarkdown for Cow<'_, str> {
245+
fn to_markdown(&self, builder: &mut MarkdownBuilder) {
246+
builder.append(&escape_markdown(self.as_ref()))
247+
}
248+
}
249+
250+
impl IntoMarkdown for Box<dyn IntoMarkdown> {
251+
fn to_markdown(&self, builder: &mut MarkdownBuilder) {
252+
self.as_ref().to_markdown(builder)
253+
}
254+
}
255+
256+
/// Usage: markdown_vec![item1, item2, item3]
257+
/// Creates Vec<dyn IntoMarkdown> from a list of items.
258+
#[macro_export]
259+
macro_rules! markdown_vec {
260+
($($x:expr),*$(,)?) => {
261+
vec![$(
262+
Box::new($x) as Box<dyn IntoMarkdown>
263+
),*]
264+
};
265+
}
266+
267+
impl<T: IntoMarkdown> IntoMarkdown for Vec<T> {
268+
fn to_markdown(&self, builder: &mut MarkdownBuilder) {
269+
for (i, item) in self.iter().enumerate() {
270+
item.to_markdown(builder);
271+
if i < self.len() - 1 {
272+
if builder.inline {
273+
builder.append(" ");
274+
} else {
275+
builder.append("\n\n");
276+
}
277+
}
278+
}
279+
}
280+
}
281+
152282
/// Builder pattern for generating comprehensive Markdown documentation.
153283
/// Now also doubles as the accumulator for the generated markdown.
154284
pub struct MarkdownBuilder {
@@ -194,8 +324,35 @@ impl MarkdownBuilder {
194324
}
195325

196326
/// Adds a paragraph element.
197-
pub fn paragraph(&mut self, text: impl Into<String>) -> &mut Self {
198-
self.elements.push(Markdown::Paragraph(text.into()));
327+
pub fn text(&mut self, text: impl Into<String>) -> &mut Self {
328+
self.elements.push(Markdown::Paragraph {
329+
text: text.into(),
330+
bold: false,
331+
italic: false,
332+
code: false,
333+
});
334+
self
335+
}
336+
337+
/// Adds a bold element.
338+
pub fn bold(&mut self, text: impl Into<String>) -> &mut Self {
339+
self.elements.push(Markdown::Paragraph {
340+
text: text.into(),
341+
bold: true,
342+
italic: false,
343+
code: false,
344+
});
345+
self
346+
}
347+
348+
/// Adds an italic element.
349+
pub fn italic(&mut self, text: impl Into<String>) -> &mut Self {
350+
self.elements.push(Markdown::Paragraph {
351+
text: text.into(),
352+
bold: false,
353+
italic: true,
354+
code: false,
355+
});
199356
self
200357
}
201358

@@ -214,13 +371,27 @@ impl MarkdownBuilder {
214371

215372
/// Adds an inline code element.
216373
pub fn inline_code(&mut self, code: impl Into<String>) -> &mut Self {
217-
self.elements.push(Markdown::InlineCode(code.into()));
374+
self.elements.push(Markdown::Paragraph {
375+
text: code.into(),
376+
bold: false,
377+
italic: false,
378+
code: true,
379+
});
218380
self
219381
}
220382

221383
/// Adds a list element.
222-
pub fn list(&mut self, ordered: bool, items: Vec<impl Into<String>>) -> &mut Self {
223-
let converted_items: Vec<String> = items.into_iter().map(|s| s.into()).collect();
384+
pub fn list(&mut self, ordered: bool, items: Vec<impl IntoMarkdown>) -> &mut Self {
385+
let converted_items: Vec<String> = items
386+
.into_iter()
387+
.map(|s| {
388+
let mut builder = MarkdownBuilder::new();
389+
builder.inline();
390+
s.to_markdown(&mut builder);
391+
builder.build()
392+
})
393+
.collect();
394+
224395
self.elements.push(Markdown::List {
225396
ordered,
226397
items: converted_items,
@@ -316,8 +487,16 @@ impl TableBuilder {
316487
}
317488

318489
/// Adds a row to the table.
319-
pub fn row(&mut self, row: Vec<impl Into<String>>) -> &mut Self {
320-
let converted: Vec<String> = row.into_iter().map(|cell| cell.into()).collect();
490+
pub fn row(&mut self, row: Vec<impl IntoMarkdown>) -> &mut Self {
491+
let converted = row
492+
.into_iter()
493+
.map(|r| {
494+
let mut builder = MarkdownBuilder::new();
495+
builder.inline();
496+
r.to_markdown(&mut builder);
497+
builder.build()
498+
})
499+
.collect();
321500
self.rows.push(converted);
322501
self
323502
}
@@ -340,17 +519,26 @@ mod tests {
340519
let mut builder = MarkdownBuilder::new();
341520
let markdown = builder
342521
.heading(1, "Documentation Title *with special chars*")
343-
.paragraph("This is the introduction with some _underscores_ and `backticks`.")
522+
.text("This is the introduction with some _underscores_ and `backticks`.")
344523
.codeblock(Some("rust"), "fn main() { println!(\"Hello, world!\"); }")
345524
.list(
346525
false,
347-
vec![
526+
markdown_vec![
348527
"First bullet with #hash",
349-
"Second bullet with [brackets]",
350-
"Third bullet with (parentheses)",
528+
Markdown::new_paragraph("Second bullet with [brackets]")
529+
.bold()
530+
.code(),
351531
],
352532
)
353533
.quote("This is a quote!\nIt spans multiple lines.")
534+
.list(
535+
true,
536+
Vec::from_iter(vec![markdown_vec![
537+
Markdown::new_paragraph("italic").italic(),
538+
Markdown::new_paragraph("bold").bold(),
539+
Markdown::new_paragraph("code").code(),
540+
]]),
541+
)
354542
.image(
355543
"Rust Logo",
356544
"https://www.rust-lang.org/logos/rust-logo-512x512.png",
@@ -361,7 +549,10 @@ mod tests {
361549
table
362550
.headers(vec!["Header 1", "Header 2"])
363551
.row(vec!["Row 1 Col 1", "Row 1 Col 2"])
364-
.row(vec!["Row 2 Col 1", "Row 2 Col 2"]);
552+
.row(markdown_vec![
553+
"Row 2 Col 1",
554+
Markdown::new_paragraph("some_code").code()
555+
]);
365556
})
366557
.build();
367558
let expected = r#"
@@ -374,12 +565,13 @@ mod tests {
374565
```
375566
376567
- First bullet with \#hash
377-
- Second bullet with \[brackets\]
378-
- Third bullet with \(parentheses\)
568+
- `Second bullet with [brackets]`
379569
380570
> This is a quote\!
381571
> It spans multiple lines\.
382572
573+
1. _italic_ **bold** `code`
574+
383575
![Rust Logo](https://www.rust-lang.org/logos/rust-logo-512x512.png)
384576
385577
[Rust Homepage](https://www.rust-lang.org)
@@ -389,7 +581,7 @@ mod tests {
389581
| Header 1 | Header 2 |
390582
| --- | --- |
391583
| Row 1 Col 1 | Row 1 Col 2 |
392-
| Row 2 Col 1 | Row 2 Col 2 |
584+
| Row 2 Col 1 | `some_code` |
393585
"#;
394586

395587
let trimmed_indentation_expected = expected

0 commit comments

Comments
 (0)