1use crate::{
2 buffer::{
3 splitter::{split, Chunk},
4 Block, Skip, Style, Token,
5 },
6 Doc, OptionParser,
7};
8
9#[cfg(feature = "docgen")]
10use crate::{
11 buffer::{extract_sections, Info, Meta},
12 meta_help::render_help,
13 Parser,
14};
15
16#[inline(never)]
17#[cfg(feature = "docgen")]
18fn collect_html(app: String, meta: &Meta, info: &Info) -> Doc {
19 let mut sections = Vec::new();
20 let root = meta;
21 let mut path = vec![app];
22 extract_sections(root, info, &mut path, &mut sections);
23
24 let mut buf = Doc::default();
25
26 if sections.len() > 1 {
27 buf.token(Token::BlockStart(Block::Block));
28 buf.token(Token::BlockStart(Block::Header));
29 buf.text("Command summary");
30 buf.token(Token::BlockEnd(Block::Header));
31 buf.token(Token::BlockEnd(Block::Block));
32
33 for section in §ions {
36 buf.token(Token::BlockStart(Block::ItemBody));
37 buf.text(&format!(
38 "* [`{}`↴](#{})",
39 section.path.join(" "),
40 section.path.join("-").to_lowercase().replace(' ', "-"),
41 ));
42 buf.token(Token::BlockEnd(Block::ItemBody));
43 }
44 }
45
46 for section in sections {
47 buf.token(Token::BlockStart(Block::Header));
48 buf.text(§ion.path.join(" ").to_string());
49 buf.token(Token::BlockEnd(Block::Header));
50
51 let b = render_help(
52 §ion.path,
53 section.info,
54 section.meta,
55 §ion.info.meta(),
56 false,
57 );
58 buf.doc(&b);
59 }
60 buf
61}
62
63impl<T> OptionParser<T> {
64 #[cfg(feature = "docgen")]
66 pub fn render_html(&self, app: impl Into<String>) -> String {
67 collect_html(app.into(), &self.inner.meta(), &self.info).render_html(true, false)
68 }
69
70 #[cfg(feature = "docgen")]
72 pub fn render_markdown(&self, app: impl Into<String>) -> String {
73 collect_html(app.into(), &self.inner.meta(), &self.info).render_markdown(true)
74 }
75}
76
77#[derive(Copy, Clone, Default)]
78pub(crate) struct Styles {
79 mono: bool,
80 bold: bool,
81 italic: bool,
82}
83impl From<Style> for Styles {
84 fn from(f: Style) -> Self {
85 match f {
86 Style::Literal => Styles {
87 bold: true,
88 mono: true,
89 italic: false,
90 },
91 Style::Metavar => Styles {
92 bold: false,
93 mono: true,
94 italic: true,
95 },
96 Style::Text => Styles {
97 bold: false,
98 mono: false,
99 italic: false,
100 },
101 Style::Emphasis | Style::Invalid => Styles {
102 mono: false,
103 bold: true,
104 italic: false,
105 },
106 }
107 }
108}
109
110fn change_style(res: &mut String, cur: &mut Styles, new: Styles) {
111 if cur.italic {
112 res.push_str("</i>");
113 }
114 if cur.bold {
115 res.push_str("</b>");
116 }
117 if cur.mono {
118 res.push_str("</tt>");
119 }
120 if new.mono {
121 res.push_str("<tt>");
122 }
123 if new.bold {
124 res.push_str("<b>");
125 }
126 if new.italic {
127 res.push_str("<i>");
128 }
129 *cur = new;
130}
131
132fn change_to_markdown_style(res: &mut String, cur: &mut Styles, new: Styles) {
133 if cur.mono {
134 res.push('`');
135 }
136
137 if cur.bold {
138 res.push_str("**");
139 }
140 if cur.italic {
141 res.push('_');
142 }
143 if new.italic {
144 res.push('_');
145 }
146 if new.bold {
147 res.push_str("**");
148 }
149 if new.mono {
150 res.push('`');
151 }
152 *cur = new;
153}
154
155fn blank_html_line(res: &mut String) {
157 if !(res.is_empty() || res.ends_with("<br>\n")) {
158 res.push_str("<br>\n");
159 }
160}
161
162fn blank_markdown_line(res: &mut String) {
164 if !(res.is_empty() || res.ends_with("\n\n")) {
165 res.push_str("\n\n");
166 }
167}
168
169fn new_markdown_line(res: &mut String) {
171 if !(res.is_empty() || res.ends_with('\n')) {
172 res.push('\n');
173 }
174}
175
176const CSS: &str = "
177<style>
178div.bpaf-doc {
179 padding: 14px;
180 background-color:var(--code-block-background-color);
181 font-family: \"Source Code Pro\", monospace;
182 margin-bottom: 0.75em;
183}
184div.bpaf-doc dt { margin-left: 1em; }
185div.bpaf-doc dd { margin-left: 3em; }
186div.bpaf-doc dl { margin-top: 0; padding-left: 1em; }
187div.bpaf-doc { padding-left: 1em; }
188</style>";
189
190impl Doc {
191 #[doc(hidden)]
192 #[must_use]
194 pub fn render_html(&self, full: bool, include_css: bool) -> String {
195 let mut res = String::new();
196 let mut byte_pos = 0;
197 let mut cur_style = Styles::default();
198
199 let mut skip = Skip::default();
202
203 let mut stack = Vec::new();
206
207 for token in self.tokens.iter().copied() {
208 match token {
209 Token::Text { bytes, style } => {
210 let input = &self.payload[byte_pos..byte_pos + bytes];
211 byte_pos += bytes;
212
213 if skip.enabled() {
214 continue;
215 }
216
217 change_style(&mut res, &mut cur_style, Styles::from(style));
218
219 for chunk in split(input) {
220 match chunk {
221 Chunk::Raw(input, _) => {
222 let input = input.replace('<', "<").replace('>', ">");
223 res.push_str(&input);
224 }
225 Chunk::Paragraph => {
226 if full {
227 res.push_str("<br>\n");
228 } else {
229 skip.enable();
230 break;
231 }
232 }
233 Chunk::LineBreak => res.push_str("<br>\n"),
234 }
235 }
236 }
237 Token::BlockStart(b) => {
238 change_style(&mut res, &mut cur_style, Styles::default());
239 match b {
240 Block::Header => {
241 blank_html_line(&mut res);
242 res.push_str("# ");
243 }
244 Block::Section2 => {
245 res.push_str("<div>\n");
246 }
247 Block::ItemTerm => res.push_str("<dt>"),
248 Block::ItemBody => {
249 if stack.last().copied() == Some(Block::DefinitionList) {
250 res.push_str("<dd>");
251 } else {
252 res.push_str("<li>");
253 }
254 }
255 Block::DefinitionList => {
256 res.push_str("<dl>");
257 }
258 Block::Block => {
259 res.push_str("<p>");
260 }
261 Block::Meta => todo!(),
262 Block::Section3 | Block::Section4 => {
263 res.push_str("<div style='padding-left: 0.5em'>")
264 }
265 Block::Mono | Block::TermRef => {}
266 Block::InlineBlock => {
267 skip.push();
268 }
269 }
270 stack.push(b);
271 }
272 Token::BlockEnd(b) => {
273 change_style(&mut res, &mut cur_style, Styles::default());
274 stack.pop();
275 match b {
276 Block::Header => {
277 blank_html_line(&mut res);
278 }
279 Block::Section2 => {
280 res.push_str("</div>");
281 }
282
283 Block::InlineBlock => {
284 skip.pop();
285 }
286 Block::ItemTerm => res.push_str("</dt>\n"),
287 Block::ItemBody => {
288 if stack.last().copied() == Some(Block::DefinitionList) {
289 res.push_str("</dd>\n");
290 } else {
291 res.push_str("</li>\n");
292 }
293 }
294 Block::DefinitionList => res.push_str("</dl>\n"),
295 Block::Block => {
296 res.push_str("</p>");
297 }
298 Block::Mono | Block::TermRef => {}
299 Block::Section3 | Block::Section4 => res.push_str("</div>"),
300 Block::Meta => todo!(),
301 }
302 }
303 }
304 }
305 change_style(&mut res, &mut cur_style, Styles::default());
306 if include_css {
307 res.push_str(CSS);
308 }
309 res
310 }
311
312 #[must_use]
314 pub fn render_markdown(&self, full: bool) -> String {
315 let mut res = String::new();
316 let mut byte_pos = 0;
317 let mut cur_style = Styles::default();
318
319 let mut skip = Skip::default();
320 let mut empty_term = false;
321 let mut mono = 0;
322 let mut def_list = false;
323 let mut code_block = false;
324 let mut app_name_seen = false;
325 for (ix, token) in self.tokens.iter().copied().enumerate() {
326 match token {
327 Token::Text { bytes, style } => {
328 let input = &self.payload[byte_pos..byte_pos + bytes];
329 byte_pos += bytes;
330 if skip.enabled() {
331 continue;
332 }
333
334 change_to_markdown_style(&mut res, &mut cur_style, Styles::from(style));
335
336 for chunk in split(input) {
337 match chunk {
338 Chunk::Raw(input, w) => {
339 if w == Chunk::TICKED_CODE {
340 new_markdown_line(&mut res);
341 res.push_str(" ");
342 res.push_str(input);
343 res.push('\n');
344 } else if w == Chunk::CODE {
345 if !code_block {
346 res.push_str("\n\n ```text\n");
347 }
348 code_block = true;
349 res.push_str(" ");
350 res.push_str(input);
351 res.push('\n');
352 } else {
353 if code_block {
354 res.push_str("\n ```\n");
355 code_block = false;
356 }
357 if mono > 0 {
358 let input = input.replace('[', "\\[").replace(']', "\\]");
359 res.push_str(&input);
360 } else {
361 res.push_str(input);
362 }
363 }
364 }
365 Chunk::Paragraph => {
366 if full {
367 res.push_str("\n\n");
368 if def_list {
369 res.push_str(" ");
370 }
371 } else {
372 skip.enable();
373 break;
374 }
375 }
376 Chunk::LineBreak => res.push('\n'),
377 }
378 }
379
380 if code_block {
381 res.push_str(" ```\n");
382 code_block = false;
383 }
384 }
385 Token::BlockStart(b) => {
386 change_to_markdown_style(&mut res, &mut cur_style, Styles::default());
387 match b {
388 Block::Header => {
389 blank_markdown_line(&mut res);
390 if app_name_seen {
391 res.push_str("## ");
392 } else {
393 res.push_str("# ");
394 app_name_seen = true;
395 }
396 }
397 Block::Section2 => {
398 res.push_str("");
399 }
400 Block::ItemTerm | Block::Section4 => {
401 new_markdown_line(&mut res);
402 empty_term = matches!(
403 self.tokens.get(ix + 1),
404 Some(Token::BlockEnd(Block::ItemTerm))
405 );
406 res.push_str(if empty_term { " " } else { "- " });
407 }
408 Block::ItemBody => {
409 if def_list {
410 res.push_str(if empty_term { " " } else { " — " });
411 }
412 new_markdown_line(&mut res);
413 res.push_str(" ");
414 }
415 Block::DefinitionList => {
416 def_list = true;
417 res.push_str("");
418 }
419 Block::Block => {
420 res.push('\n');
421 }
422 Block::Meta => todo!(),
423 Block::Mono => {
424 mono += 1;
425 }
426 Block::Section3 => res.push_str("### "),
427 Block::TermRef => {}
428 Block::InlineBlock => {
429 skip.push();
430 }
431 }
432 }
433 Token::BlockEnd(b) => {
434 change_to_markdown_style(&mut res, &mut cur_style, Styles::default());
435 match b {
436 Block::Header
437 | Block::Block
438 | Block::Section3
439 | Block::Section2
440 | Block::Section4 => {
441 res.push('\n');
442 }
443
444 Block::InlineBlock => {
445 skip.pop();
446 }
447 Block::ItemTerm | Block::TermRef => {}
448 Block::ItemBody => {
449 if def_list {
450 res.push('\n');
451 }
452 }
453 Block::DefinitionList => {
454 def_list = false;
455 res.push('\n');
456 }
457 Block::Mono => {
458 mono -= 1;
459 }
460 Block::Meta => todo!(),
461 }
462 }
463 }
464 }
465 change_to_markdown_style(&mut res, &mut cur_style, Styles::default());
466 res
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn transitions_are_okay() {
476 let mut doc = Doc::default();
477
478 doc.emphasis("Usage: "); doc.literal("my_program"); let r = doc.render_html(true, false);
482
483 assert_eq!(r, "<b>Usage: </b><tt><b>my_program</b></tt>")
484 }
485}