1use std::path::{Path, PathBuf};
2
3use roff::{bold, italic, roman, Inline, Roff};
4
5const MANUAL: &str = "User Commands";
9
10const SOURCE: &str = "Sequoia PGP";
12
13macro_rules! warn {
15 {$($exp: expr),*} => {
16 println!("cargo:warning={}",
17 format_args!($($exp),*));
18 };
19}
20
21macro_rules! fail {
23 {$($exp: expr),*} => {
24 warn!($($exp),*);
25 std::process::exit(1);
26 };
27}
28
29pub struct Builder {
37 title: String,
38 section: String,
39 date: Option<String>,
40 source: Option<String>,
41 manual: Option<String>,
42 version: Option<String>,
43 see_also: Vec<String>,
44 maincmd: Command,
45 top_level_global_args: Vec<CommandOption>,
48}
49
50impl Builder {
51 pub fn new(cmd: &mut clap::Command,
66 version: &str, extra_version: Option<&str>)
67 -> Self
68 {
69 cmd.build();
70
71 let version = if let Some(extra_version) = extra_version {
72 format!("{} ({})", version, extra_version)
73 } else {
74 version.to_string()
75 };
76
77 let maincmd = Command::from_command(&[], cmd);
78 let options = maincmd.get_options();
79
80 let top_level_global_args = options.into_iter()
83 .filter(|a| a.global || a.long.as_deref() == Some("--help"))
84 .collect::<Vec<_>>();
85
86 Self {
87 title: cmd.get_name().into(),
88 section: "1".into(),
89 date: Some(version.clone()),
90 source: Some(SOURCE.to_string()),
91 manual: Some(MANUAL.to_string()),
92 version: Some(version),
93 see_also: Vec::new(),
94 maincmd,
95 top_level_global_args,
96 }
97 }
98
99 fn is_top_level_global_arg(&self, arg: &CommandOption) -> bool {
102 self.top_level_global_args
103 .binary_search_by_key(&arg.sort_key(),
104 |a: &CommandOption| a.sort_key())
105 .is_ok()
106 }
107
108 pub fn date(&mut self, date: &str) -> &mut Self{
111 self.date = Some(date.into());
112 self
113 }
114
115 pub fn source(&mut self, source: &str) -> &mut Self {
118 self.source = Some(source.into());
119 self
120 }
121
122 pub fn manual(&mut self, manual: &str) -> &mut Self {
125 self.manual = Some(manual.into());
126 self
127 }
128
129 fn summary(about: &str) -> String {
132 let line = if let Some(line) = about.lines().next() {
133 line
134 } else {
135 ""
136 };
137 line.to_string()
138 }
139
140 pub fn see_also<S>(&mut self, see_also: &[S]) -> &mut Self
148 where S: AsRef<str>
149 {
150 self.see_also = see_also.into_iter()
151 .map(|s| {
152 s.as_ref().to_string()
153 })
154 .collect();
155 self
156 }
157
158 pub fn binary(&self) -> String {
160 self.maincmd.name()
161 }
162
163 pub fn build(&self) -> Vec<ManualPage> {
165 let mut pages = vec![];
166 self.maincmd.build(self, &mut pages);
167 pages
168 }
169
170 fn th(&self, man: &mut ManualPage) {
172 let empty = String::new();
173 man.th(
174 &self.title.to_uppercase(),
175 &self.section.to_uppercase(),
176 self.date.as_ref().unwrap_or(&empty),
177 self.source.as_ref().unwrap_or(&empty),
178 self.manual.as_ref().unwrap_or(&empty),
179 )
180 }
181}
182
183#[derive(Debug, PartialEq, Eq)]
189struct Command {
190 command_words: Vec<String>,
191 before_help: Option<String>,
192 after_help: Option<String>,
193 about: Option<String>,
194 long_about: Option<String>,
195 options: Vec<CommandOption>,
196 args: Vec<String>,
197 examples: Vec<String>,
198 exit_status: Vec<String>,
199 subcommands: Vec<Command>,
200}
201
202impl Command {
203 fn new(command_words: Vec<String>) -> Self {
208 assert!(! command_words.is_empty());
209 Self {
210 command_words,
211 before_help: None,
212 after_help: None,
213 about: None,
214 long_about: None,
215 options: vec![],
216 args: vec![],
217 examples: vec![],
218 exit_status: vec![],
219 subcommands: vec![],
220 }
221 }
222
223 fn top_level(&self) -> bool {
225 self.command_words.len() == 1
226 }
227
228 fn leaf(&self) -> bool {
232 ! self.top_level() && self.subcommands.is_empty()
233 }
234
235 fn singleton(&self) -> bool {
238 self.top_level() && self.subcommands.is_empty()
239 }
240
241 fn name(&self) -> String {
244 self.command_words.join(" ")
245 }
246
247 fn subcommand_name(&self) -> String {
249 let mut words = self.command_words.clone();
250 words.remove(0);
251 words.join(" ")
252 }
253
254 fn manpage_name(&self) -> String {
259 self.command_words.join("-")
260 }
261
262 fn description(&self) -> String {
265 let mut desc = String::new();
266 if let Some(text) = &self.before_help {
267 desc.push_str(text);
268 desc.push('\n');
269 }
270
271 if let Some(text) = &self.long_about {
272 desc.push_str(text);
273 desc.push('\n');
274 } else if let Some(text) = &self.about {
275 desc.push_str(text);
276 desc.push('\n');
277 }
278
279 if let Some(text) = &self.after_help {
280 desc.push_str(text);
281 desc.push('\n');
282 }
283 desc
284 }
285
286 fn before_help(&mut self, help: &str) {
288 self.before_help = Some(self.extract_all(help));
289 }
290
291 fn after_help(&mut self, help: &str) {
293 self.after_help = Some(self.extract_all(help));
294 }
295
296 fn about(&mut self, help: &str) {
298 self.about = Some(self.extract_all(help));
299 }
300
301 fn long_about(&mut self, help: &str) {
303 self.long_about = Some(self.extract_all(help));
304 }
305
306 fn option(&mut self, opt: CommandOption) {
308 self.options.push(opt);
309 }
310
311 fn arg(&mut self, arg: &str) {
313 self.args.push(arg.into());
314 }
315
316 fn extract_all(&mut self, text: &str) -> String {
318 let text = self.extract_example(text);
319 let text = self.extract_exit_status(&text);
320 text
321 }
322
323 fn extract_example(&mut self, text: &str) -> String {
329 Self::extract("Examples:\n", &mut self.examples, text)
330 }
331
332 fn extract_exit_status(&mut self, text: &str) -> String {
338 Self::extract("Exit status:\n", &mut self.exit_status, text)
339 }
340
341 fn extract(marker: &str, into: &mut Vec<String>, text: &str) -> String {
343 if let Some(pos) = text.find(marker) {
344 let (text, ex) = text.split_at(pos);
345 if let Some(ex) = ex.strip_prefix(marker) {
346 into.push(ex.into());
347 } else {
348 into.push(ex.into());
349 }
350 text.into()
351 } else {
352 text.into()
353 }
354 }
355
356 fn has_options(&self) -> bool {
358 !self.options.is_empty()
359 }
360
361 fn get_options(&self) -> Vec<CommandOption> {
363 let mut opts = self.options.clone();
364 opts.sort_by_cached_key(|opt| opt.sort_key());
365 opts
366 }
367
368 fn has_examples(&self) -> bool {
370 !self.examples.is_empty()
371 }
372
373 fn from_command(parent: &[String], cmd: &clap::Command) -> Self {
375 let mut words: Vec<String> = parent.into();
376 words.push(cmd.get_name().to_string());
377 let mut new = Self::new(words);
378 if let Some(text) = cmd.get_before_help() {
379 new.before_help(&text.to_string());
380 }
381 if let Some(text) = cmd.get_after_help() {
382 new.after_help(&text.to_string());
383 }
384 if let Some(text) = cmd.get_about() {
385 new.about(&text.to_string());
386 }
387 if let Some(text) = cmd.get_long_about() {
388 new.long_about(&text.to_string());
389 }
390 for arg in cmd.get_arguments()
391 .filter(|a| ! a.is_hide_set())
392 {
393 new.option(CommandOption::from_arg(arg));
394 }
395 for arg in cmd.get_positionals()
396 .filter(|a| ! a.is_hide_set())
397 {
398 if let Some(names) = arg.get_value_names() {
399 for name in names {
400 new.arg(name);
401 }
402 }
403 }
404 let mut parent = parent.to_vec();
405 parent.push(cmd.get_name().into());
406 new.subcommands = cmd.get_subcommands()
407 .filter(|cmd| ! cmd.is_hide_set())
408 .filter(|cmd| cmd.get_name() != "help")
409 .map(|cmd| Command::from_command(&parent, cmd))
410 .collect();
411 new
412 }
413
414 fn build(&self, builder: &Builder, acc: &mut Vec<ManualPage>) {
416 acc.push(self.build_command(builder));
417 for cmd in &self.subcommands {
418 cmd.build(builder, acc);
419 }
420 }
421
422 fn build_command(&self, builder: &Builder) -> ManualPage {
424 let filename = format!("{}.{}", self.manpage_name(), builder.section);
425 let mut man = ManualPage::new(PathBuf::from(filename));
426 builder.th(&mut man);
427
428 let about = &self.about.clone().unwrap();
429 let summary = Builder::summary(about);
430 man.name_section(&self.manpage_name(), &summary);
431
432 man.section("SYNOPSIS");
433 let bin_name = builder.binary();
434 if self.singleton() || self.leaf() {
435 man.subcommand_synopsis(
437 &bin_name,
438 &self.subcommand_name(),
439 self.has_options(),
440 &self.args,
441 true,
442 );
443 } else {
444 for sub in &self.subcommands {
446 man.subcommand_synopsis(
447 &bin_name,
448 &sub.subcommand_name(),
449 sub.has_options(),
450 &sub.args,
451 sub.leaf(),
452 );
453 }
454 }
455
456 man.section("DESCRIPTION");
457 man.text_with_period(&self.description());
458
459 if self.top_level() || self.leaf() {
461 let mut showed_options_header = false;
462
463 let mut self_opts = self.get_options();
465 if ! self.singleton() {
466 self_opts = self_opts.into_iter()
467 .filter(|o| ! builder.is_top_level_global_arg(o))
468 .collect::<Vec<_>>();
469 }
470
471 if ! self_opts.is_empty() {
472 if ! showed_options_header {
473 showed_options_header = true;
474 man.section("OPTIONS");
475 }
476
477 if ! self.top_level() {
478 man.subsection("Subcommand options");
480 }
481
482 for opt in self_opts.iter() {
483 man.option(opt);
484 }
485 }
486
487 let global_options = &builder.top_level_global_args;
490 if ! self.singleton() && ! global_options.is_empty() {
491 #[allow(unused_assignments)]
492 if ! showed_options_header {
493 showed_options_header = true;
494 man.section("OPTIONS");
495 }
496
497 man.subsection("Global options");
498 if self.top_level() {
499 for opt in global_options.iter() {
502 man.option(opt);
503 }
504 } else {
505 man.roff.text(vec![
508 roman("See "),
509 bold(bin_name), roman("("), roman("1"), roman(")"),
510 roman(" for a description of the global options."),
511 ]);
512 }
513 }
514 }
515
516 if ! self.subcommands.is_empty() {
517 man.section("SUBCOMMANDS");
518 for sub in &self.subcommands {
519 let desc = sub.description();
520 if !desc.is_empty() {
521 man.subsection(&sub.name());
522 man.text_with_period(&desc);
523 }
524 }
525 }
526
527 man.exit_status_section(self);
528
529 if self == &builder.maincmd {
531 let opts = self.get_options();
532 let mut envs: Vec<_> =
533 opts.iter().filter(|o| o.env.is_some()).collect();
534 envs.sort_by_key(|o| o.env.as_ref());
535
536 if ! envs.is_empty() {
537 man.section("ENVIRONMENT");
538 for opt in envs {
539 man.env_option(opt);
540 }
541 }
542 }
543
544 if self.singleton() || self.leaf() {
545 man.examples_section(&[self]);
546 } else {
547 man.examples_section(
548 &std::iter::once(self)
549 .chain(self.subcommands.iter())
550 .collect::<Vec<_>>());
551 }
552
553 let mut see_also_shown = false;
554 let names: Vec<String> = (1..self.command_words.len())
555 .map(|n| self.command_words[0..n].join("-"))
556 .chain(self.subcommands.iter().map(|sub| sub.manpage_name()))
557 .collect();
558 if ! names.is_empty() {
559 if ! see_also_shown {
560 man.section("SEE ALSO");
561 see_also_shown = true;
562 }
563
564 man.man_page_refs(&names, &builder.section);
565 man.paragraph();
566 }
567 if ! builder.see_also.is_empty() {
568 #[allow(unused_assignments)]
569 if ! see_also_shown {
570 man.section("SEE ALSO");
571 see_also_shown = true;
572 }
573
574 for (i, see_also) in builder.see_also.iter().enumerate() {
575 if i > 0 {
576 man.paragraph();
577 }
578 man.text(see_also);
579 }
580 }
581
582 man.version_section(&builder.version);
583
584 man
585 }
586}
587
588#[derive(Clone, Debug, PartialEq, Eq)]
593struct CommandOption {
594 index: Option<usize>,
596 short: Option<String>,
597 long: Option<String>,
598 global: bool,
600 env: Option<String>,
601 value_names: Values,
602 possible_values: Vec<String>,
603 default_values: Vec<String>,
604 help: Option<String>,
605}
606
607#[derive(Clone, Debug, PartialEq, Eq)]
609enum Values {
610 None,
612
613 Some(Vec<String>),
615
616 Optional(Vec<String>),
622}
623
624impl CommandOption {
625 fn sort_key(&self) -> (usize, String) {
634 let mut key = String::new();
635 if let Some(name) = &self.short {
636 key.push_str(name.strip_prefix('-').unwrap());
637 key.push(',');
638 }
639 if let Some(name) = &self.long {
640 key.push_str(name.strip_prefix("--").unwrap());
641 }
642 (self.index.unwrap_or(0), key)
643 }
644}
645
646impl CommandOption {
647 fn from_arg(arg: &clap::Arg) -> Self {
649 let num_args = arg.get_num_args().unwrap_or_default();
650 let value_names = if num_args.takes_values() {
651 let names = arg.get_value_names()
652 .unwrap_or(&[])
653 .into_iter()
654 .map(|s| s.to_string())
655 .collect::<Vec<String>>();
656
657 if num_args.min_values() == 0 {
658 Values::Optional(names)
659 } else {
660 Values::Some(names)
661 }
662 } else {
663 Values::None
665 };
666
667 let long = arg.get_long().map(|o| format!("--{}", o));
668 let global = arg.is_global_set();
669 if global {
670 assert!(long.is_some(),
671 "Global options must have a long name.");
672 }
673
674 Self {
675 index: arg.get_index(),
676 short: arg.get_short().map(|o| format!("-{}", o)),
677 long,
678 global,
679 value_names,
680 env: arg.get_env().and_then(|e| e.to_str().map(Into::into)),
681 possible_values: arg.get_possible_values().iter()
682 .map(|o| o.get_name().into()).collect(),
683 default_values: if num_args.takes_values() {
684 arg.get_default_values().iter()
686 .map(|s| s.to_string_lossy().to_string())
687 .collect()
688 } else {
689 vec![]
692 },
693 help: arg.get_long_help().or(arg.get_help())
694 .map(|s| s.to_string()),
695 }
696 }
697}
698
699pub struct ManualPage {
705 filename: PathBuf,
706 roff: Roff,
707
708 in_code: bool,
710}
711
712impl ManualPage {
713 fn new(filename: PathBuf) -> Self {
714 Self {
715 filename,
716 roff: Roff::new(),
717 in_code: false,
718 }
719 }
720
721 fn th(&mut self, name: &str, section: &str, date: &str, source: &str, manual: &str) {
725 self.roff
726 .control("TH", [name, section, date, source, manual]);
727 }
728
729 fn name_section(&mut self, name: &str, summary: &str) {
734 self.section("NAME");
735 self.roff.text([roman(&format!("{} - {}", name, summary))]);
736 }
737
738 fn code(&mut self, code: bool) {
740 match (self.in_code, code) {
741 (false, false) => (),
742 (false, true) => {
743 self.roff.control("nf", []);
744 self.in_code = true;
745 },
746 (true, false) => {
747 self.roff.control("fi", []);
748 self.in_code = false;
749 },
750 (true, true) => (),
751 }
752 }
753
754 fn subcommand_synopsis(
760 &mut self,
761 bin: &str,
762 sub: &str,
763 sub_options: bool,
764 args: &[String],
765 is_leaf: bool,
766 ) {
767 let mut line = vec![
768 bold(if sub.is_empty() {
769 bin.to_string()
770 } else {
771 format!("{} {}", bin, sub)
772 }),
773 roman(" ["), italic("OPTIONS"), roman("] "),
774 ];
775
776 for (i, arg) in args.iter().enumerate() {
777 if i > 0 || ! sub_options {
778 line.push(roman(" "));
779 }
780 line.push(italic(arg));
781 }
782
783 if args.is_empty() {
784 line.push(roman(" "));
785 }
786
787 if ! is_leaf {
788 line.push(italic("SUBCOMMAND"));
789 }
790
791 self.roff.control("br", []);
792 self.roff.text(line);
793 }
794
795 fn option(&mut self, opt: &CommandOption) {
800 let mut line = vec![];
801
802 if let Some(short) = &opt.short {
803 line.push(bold(short));
804 }
805 if let Some(long) = &opt.long {
806 if opt.short.is_some() {
807 line.push(roman(", "));
808 }
809 line.push(bold(long));
810 }
811
812 match &opt.value_names {
813 Values::None => (),
814 Values::Some(values) | Values::Optional(values) => {
815 if matches!(opt.value_names, Values::Optional(_)) {
816 line.push(roman("["));
817 }
818
819 if (opt.short.is_some() || opt.long.is_some())
820 && values.len() == 1
821 {
822 line.push(roman("="));
823 line.push(italic(&values[0]));
824 } else {
825 for value in values {
826 line.push(roman(" "));
827 line.push(italic(value));
828 }
829 }
830
831 if matches!(opt.value_names, Values::Optional(_)) {
832 line.push(roman("]"));
833 }
834 },
835 }
836
837 self.tagged_paragraph(line, &opt.help);
838
839 if ! opt.default_values.is_empty() {
840 self.indented_paragraph();
841 let mut line = vec![];
842 line.push(roman("[default: "));
843 for (i, v) in opt.default_values.iter().enumerate() {
844 if i > 0 {
845 line.push(roman(", "));
846 }
847 line.push(bold(v));
848 }
849 line.push(roman("]"));
850 self.roff.text(line);
851 }
852
853 if ! opt.possible_values.is_empty() {
854 self.indented_paragraph();
855 let mut line = vec![];
856 line.push(roman("[possible values: "));
857 for (i, v) in opt.possible_values.iter().enumerate() {
858 if i > 0 {
859 line.push(roman(", "));
860 }
861 line.push(bold(v));
862 }
863 line.push(roman("]"));
864 self.roff.text(line);
865 }
866 }
867
868 fn env_option(&mut self, opt: &CommandOption) {
875 let mut line = vec![
876 bold(opt.env.as_ref().expect("must be an env")),
877 ];
878
879 match &opt.value_names {
880 Values::None => (),
881 Values::Some(values) | Values::Optional(values) => {
882 assert_eq!(values.len(), 1);
883 line.push(roman("="));
884 line.push(italic(&values[0]));
885 },
886 }
887
888 self.tagged_paragraph(line, &opt.help);
889
890 if ! opt.default_values.is_empty() {
891 self.indented_paragraph();
892 let mut line = vec![];
893 line.push(roman("[default: "));
894 for (i, v) in opt.default_values.iter().enumerate() {
895 if i > 0 {
896 line.push(roman(", "));
897 }
898 line.push(bold(v));
899 }
900 line.push(roman("]"));
901 self.roff.text(line);
902 }
903
904 if ! opt.possible_values.is_empty() {
905 self.indented_paragraph();
906 let mut line = vec![];
907 line.push(roman("[possible values: "));
908 for (i, v) in opt.possible_values.iter().enumerate() {
909 if i > 0 {
910 line.push(roman(", "));
911 }
912 line.push(bold(v));
913 }
914 line.push(roman("]"));
915 self.roff.text(line);
916 }
917 }
918
919 fn exit_status_section(&mut self, c: &Command) {
921 if c.exit_status.is_empty() {
922 return;
923 }
924
925 self.section("EXIT STATUS");
926 for chunk in &c.exit_status {
927 self.text(&chunk);
928 }
929 }
930
931 fn examples_section(&mut self, subs: &[&Command]) {
933 if !subs.iter().any(|c| c.has_examples()) {
934 return;
935 }
936
937 self.section("EXAMPLES");
938 let mut need_para = false;
939 let need_subsections = subs.len() > 1;
940 for cmd in subs.iter() {
941 if need_para {
942 self.paragraph();
943 need_para = false;
944 }
945
946 if !cmd.examples.is_empty() {
947 if need_subsections {
948 self.subsection(&cmd.name());
949 need_para = false;
950 }
951
952 for ex in cmd.examples.iter() {
953 let mut continuation = false;
955
956 let mut description = false;
958
959 for line in ex.lines() {
960 if ! continuation
961 && ! (description && line.starts_with("#"))
962 {
963 self.paragraph();
964 }
965
966 const TARGET_LINE_LENGTH: usize = 78;
967 const RS_INDENTATION: usize = 7;
968 const EXAMPLE_COMMAND_MAX_WIDTH: usize =
969 TARGET_LINE_LENGTH - 2 * RS_INDENTATION;
970 const EXAMPLE_CONTINUATION_MAX_WIDTH: usize =
971 TARGET_LINE_LENGTH - 3 * RS_INDENTATION;
972
973 if let Some(line) = line.strip_prefix("# ") {
974 self.code(false);
975 self.roff.text([roman(line)]);
976 } else if let Some(line) = line.strip_prefix("$ ") {
977 let line = line.trim();
978 if line.len() > EXAMPLE_COMMAND_MAX_WIDTH {
979 warn!("Command in example exceeds {} chars:",
980 EXAMPLE_COMMAND_MAX_WIDTH);
981 fail!("{} ({} chars)", line, line.len());
982 }
983 self.code(true);
984 self.roff.control("RS", []);
985 self.roff.text([roman(line)]);
986 self.roff.control("RE", []);
987 } else if continuation {
988 let line = line.trim();
989 if line.len() > EXAMPLE_CONTINUATION_MAX_WIDTH {
990 warn!("Continuation in example exceeds {} chars:",
991 EXAMPLE_CONTINUATION_MAX_WIDTH);
992 fail!("{} ({} chars)", line, line.len());
993 }
994 self.code(true);
995 self.roff.control("RS", []);
996 self.roff.control("RS", []);
997 self.roff.text([roman(line)]);
998 self.roff.control("RE", []);
999 self.roff.control("RE", []);
1000 } else {
1001 self.code(false);
1002 self.roff.text([roman(line)]);
1003 }
1004
1005 continuation = line.ends_with("\\");
1007
1008 description = line.starts_with("#");
1010
1011 need_para = true;
1015 }
1016 }
1017
1018 self.code(false);
1019 }
1020 }
1021 }
1022
1023 fn version_section(&mut self, version: &Option<String>) {
1026 if let Some(v) = version {
1027 self.section("VERSION");
1028 self.roff.text([roman(v)]);
1029 }
1030 }
1031
1032 fn section(&mut self, heading: &str) {
1034 self.roff.control("SH", [heading]);
1035 }
1036
1037 fn subsection(&mut self, heading: &str) {
1039 self.roff.control("SS", [heading]);
1040 }
1041
1042 fn paragraph(&mut self) {
1044 self.roff.control("PP", []);
1045 }
1046
1047 fn indented_paragraph(&mut self) {
1049 self.roff.control("IP", []);
1050 }
1051
1052 fn tagged_paragraph(&mut self, line: Vec<Inline>, text: &Option<String>) {
1060 self.roff.control("TP", []);
1061 self.roff.text(line);
1062
1063 if let Some(text) = text {
1064 let mut paras = text.split("\n\n");
1065 if let Some(first) = paras.next() {
1066 self.roff.text([roman(first)]);
1067 }
1068 for para in paras {
1069 self.roff.control("IP", []);
1070 self.roff.text([roman(para)]);
1071 }
1072 }
1073 }
1074
1075 fn man_page_refs(&mut self, names: &[String], section: &str) {
1081 let mut line = vec![];
1082 for name in names.iter() {
1083 if !line.is_empty() {
1084 line.push(roman(", "));
1085 }
1086 line.push(bold(name));
1087 line.push(roman("("));
1088 line.push(roman(section));
1089 line.push(roman(")"));
1090 }
1091 line.push(roman("."));
1092
1093 self.roff.control("nh", []);
1094 self.roff.text(line);
1095 self.roff.control("hy", []);
1096 }
1097
1098 fn text(&mut self, text: &str) {
1103 let mut paras = text.split("\n\n");
1104 if let Some(first) = paras.next() {
1105 self.roff.text([roman(first)]);
1106 }
1107 for para in paras {
1108 self.paragraph();
1109 self.roff.text([roman(para)]);
1110 }
1111 }
1112
1113 fn text_with_period(&mut self, text: &str) {
1119 let mut paras = text.split("\n\n");
1120 if let Some(first) = paras.next() {
1121 let first = if let Some(prefix) = first.strip_suffix(".\n") {
1122 format!("{}.", prefix)
1123 } else if let Some(prefix) = first.strip_suffix('\n') {
1124 format!("{}.", prefix)
1125 } else if first.ends_with('.') {
1126 first.to_string()
1127 } else {
1128 format!("{}.", first)
1129 };
1130 self.roff.text([roman(first)]);
1131 }
1132 for para in paras {
1133 self.paragraph();
1134 self.roff.text([roman(para)]);
1135 }
1136 }
1137
1138 pub fn filename(&self) -> &Path {
1140 &self.filename
1141 }
1142
1143 pub fn troff_source(&self) -> String {
1145 self.roff.to_roff()
1146 }
1147}