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 => res.push_str("<div style='padding-left: 0.5em'>"),
263 Block::Mono | Block::TermRef => {}
264 Block::InlineBlock => {
265 skip.push();
266 }
267 }
268 stack.push(b);
269 }
270 Token::BlockEnd(b) => {
271 change_style(&mut res, &mut cur_style, Styles::default());
272 stack.pop();
273 match b {
274 Block::Header => {
275 blank_html_line(&mut res);
276 }
277 Block::Section2 => {
278 res.push_str("</div>");
279 }
280
281 Block::InlineBlock => {
282 skip.pop();
283 }
284 Block::ItemTerm => res.push_str("</dt>\n"),
285 Block::ItemBody => {
286 if stack.last().copied() == Some(Block::DefinitionList) {
287 res.push_str("</dd>\n");
288 } else {
289 res.push_str("</li>\n");
290 }
291 }
292 Block::DefinitionList => res.push_str("</dl>\n"),
293 Block::Block => {
294 res.push_str("</p>");
295 }
296 Block::Mono | Block::TermRef => {}
297 Block::Section3 => res.push_str("</div>"),
298 Block::Meta => todo!(),
299 }
300 }
301 }
302 }
303 change_style(&mut res, &mut cur_style, Styles::default());
304 if include_css {
305 res.push_str(CSS);
306 }
307 res
308 }
309
310 #[must_use]
312 pub fn render_markdown(&self, full: bool) -> String {
313 let mut res = String::new();
314 let mut byte_pos = 0;
315 let mut cur_style = Styles::default();
316
317 let mut skip = Skip::default();
318 let mut empty_term = false;
319 let mut mono = 0;
320 let mut def_list = false;
321 let mut code_block = false;
322 let mut app_name_seen = false;
323 for (ix, token) in self.tokens.iter().copied().enumerate() {
324 match token {
325 Token::Text { bytes, style } => {
326 let input = &self.payload[byte_pos..byte_pos + bytes];
327 byte_pos += bytes;
328 if skip.enabled() {
329 continue;
330 }
331
332 change_to_markdown_style(&mut res, &mut cur_style, Styles::from(style));
333
334 for chunk in split(input) {
335 match chunk {
336 Chunk::Raw(input, w) => {
337 if w == Chunk::TICKED_CODE {
338 new_markdown_line(&mut res);
339 res.push_str(" ");
340 res.push_str(input);
341 res.push('\n');
342 } else if w == Chunk::CODE {
343 if !code_block {
344 res.push_str("\n\n ```text\n");
345 }
346 code_block = true;
347 res.push_str(" ");
348 res.push_str(input);
349 res.push('\n');
350 } else {
351 if code_block {
352 res.push_str("\n ```\n");
353 code_block = false;
354 }
355 if mono > 0 {
356 let input = input.replace('[', "\\[").replace(']', "\\]");
357 res.push_str(&input);
358 } else {
359 res.push_str(input);
360 }
361 }
362 }
363 Chunk::Paragraph => {
364 if full {
365 res.push_str("\n\n");
366 if def_list {
367 res.push_str(" ");
368 }
369 } else {
370 skip.enable();
371 break;
372 }
373 }
374 Chunk::LineBreak => res.push('\n'),
375 }
376 }
377
378 if code_block {
379 res.push_str(" ```\n");
380 code_block = false;
381 }
382 }
383 Token::BlockStart(b) => {
384 change_to_markdown_style(&mut res, &mut cur_style, Styles::default());
385 match b {
386 Block::Header => {
387 blank_markdown_line(&mut res);
388 if app_name_seen {
389 res.push_str("## ");
390 } else {
391 res.push_str("# ");
392 app_name_seen = true;
393 }
394 }
395 Block::Section2 => {
396 res.push_str("");
397 }
398 Block::ItemTerm => {
399 new_markdown_line(&mut res);
400 empty_term = matches!(
401 self.tokens.get(ix + 1),
402 Some(Token::BlockEnd(Block::ItemTerm))
403 );
404 res.push_str(if empty_term { " " } else { "- " });
405 }
406 Block::ItemBody => {
407 if def_list {
408 res.push_str(if empty_term { " " } else { " — " });
409 }
410 new_markdown_line(&mut res);
411 res.push_str(" ");
412 }
413 Block::DefinitionList => {
414 def_list = true;
415 res.push_str("");
416 }
417 Block::Block => {
418 res.push('\n');
419 }
420 Block::Meta => todo!(),
421 Block::Mono => {
422 mono += 1;
423 }
424 Block::Section3 => res.push_str("### "),
425 Block::TermRef => {}
426 Block::InlineBlock => {
427 skip.push();
428 }
429 }
430 }
431 Token::BlockEnd(b) => {
432 change_to_markdown_style(&mut res, &mut cur_style, Styles::default());
433 match b {
434 Block::Header | Block::Block | Block::Section3 | Block::Section2 => {
435 res.push('\n');
436 }
437
438 Block::InlineBlock => {
439 skip.pop();
440 }
441 Block::ItemTerm | Block::TermRef => {}
442 Block::ItemBody => {
443 if def_list {
444 res.push('\n');
445 }
446 }
447 Block::DefinitionList => {
448 def_list = false;
449 res.push('\n');
450 }
451 Block::Mono => {
452 mono -= 1;
453 }
454 Block::Meta => todo!(),
455 }
456 }
457 }
458 }
459 change_to_markdown_style(&mut res, &mut cur_style, Styles::default());
460 res
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn transitions_are_okay() {
470 let mut doc = Doc::default();
471
472 doc.emphasis("Usage: "); doc.literal("my_program"); let r = doc.render_html(true, false);
476
477 assert_eq!(r, "<b>Usage: </b><tt><b>my_program</b></tt>")
478 }
479}