Skip to main content

markdown_formatter/
args.rs

1pub mod builder;
2pub mod split;
3
4use std::{cmp::max, process};
5
6use crate::args::builder::{print_doc, print_help, CommandLineOption};
7
8/// 根据命令行的输入内容,生成相应的配置选项,用于调整 Markdowon Formatter 的行为。
9#[derive(Debug)]
10pub struct Args {
11    /// 需要处理的 Markdown 源文件路径
12    pub input: String,
13
14    /// 调试标志位,将输出每一个可能的中间值
15    pub debug: bool,
16
17    /// 中文冒号转为英文冒号+空格,“:” => “: ”
18    pub flag_colon: bool,
19
20    /// 表格的每一行两端使用 |
21    pub flag_enclose_table: bool,
22
23    /// 从段落提取链接用于填充在段末或文章结尾
24    pub flag_extract_links: bool,
25
26    /// 提取链接时去掉锚点部分
27    pub flag_extract_links_without_anchor: bool,
28
29    /// 使用 Google 风格的 List 缩进,参见
30    /// <https://google.github.io/styleguide/docguide/style.html>
31    pub flag_google_list_style: bool,
32
33    /// 中文分号转为英文分号+空格,“;” => “; ”
34    pub flag_semicolon: bool,
35
36    /// 中文括号转为英文括号+空格,“()” => “ () ”,将根据上下文去除多余的空格
37    pub flag_parenthesis: bool,
38
39    /// 以相同列宽展示表格中的每一列
40    pub flag_pretty_table: bool,
41
42    /// 去除 <url> 内部的非必要空格
43    pub flag_strip_enclosed_url: bool,
44
45    /// 加粗文字前后使用空格,“a**b**c” => “a **b** c”,将根据上下文去除多余的空格
46    pub flag_whitespace_around_bold_font: bool,
47
48    /// 斜体字更改为加粗文字,“a*b*c” => “a**b**c”
49    pub flag_italic_to_bold_font: bool,
50
51    /// 是否需要在开始格式化之前输出相应的配置
52    pub print_config: bool,
53
54    /// 输出配置时使用紧凑的布局
55    pub compact_output: bool,
56
57    /// 执行 Markdown 模块的函数时输出成对的 Tag
58    pub debug_markdown_tag_pair: bool,
59}
60
61impl Args {
62    fn new() -> Self {
63        Self {
64            input: String::from(""),
65            debug: false,
66            flag_colon: false,
67            flag_enclose_table: false,
68            flag_semicolon: false,
69            flag_parenthesis: false,
70            flag_pretty_table: false,
71            flag_extract_links: false,
72            flag_extract_links_without_anchor: false,
73            flag_whitespace_around_bold_font: false,
74            flag_italic_to_bold_font: false,
75            flag_strip_enclosed_url: false,
76            print_config: false,
77            compact_output: false,
78            flag_google_list_style: false,
79            debug_markdown_tag_pair: false,
80        }
81    }
82
83    /// 解析命令行选项和参数
84    ///
85    /// # Arguments
86    /// - `args` - 来自命令行或认为构建的选项
87    pub fn parse(args: Vec<String>) -> Option<Self> {
88        if args.len() > 1 {
89            let mut val = Self::new();
90            val.input = args.last().unwrap().to_string();
91
92            let mut options = vec![];
93
94            for arg in args {
95                let mut should_explain = true;
96
97                match arg.as_str() {
98                    // Help
99                    "--help" => {
100                        print_help_info(Some(true), Some(options));
101                        process::exit(0)
102                    }
103                    "-h" => {
104                        print_help_info(None, None);
105                        process::exit(0)
106                    }
107                    "--print" | "-p" => val.print_config = true,
108                    "--print-compact" => {
109                        val.compact_output = true;
110                        val.print_config = true
111                    }
112                    // Flags
113                    "--colon" => val.flag_colon = true,
114                    "--extract-links" => val.flag_extract_links = true,
115                    "--enclose-table" => val.flag_enclose_table = true,
116                    "--extract-no-anchor" => val.flag_extract_links_without_anchor = true,
117                    "--google-list-style" => val.flag_google_list_style = true,
118                    "--italic-to-bold" => val.flag_italic_to_bold_font = true,
119                    "--parenthesis" => val.flag_parenthesis = true,
120                    "--semicolon" => val.flag_semicolon = true,
121                    "--pretty-table" => {
122                        val.flag_pretty_table = true;
123                        panic!("Unimplemented style")
124                    }
125                    "--whitespace-around-bold" => val.flag_whitespace_around_bold_font = true,
126                    "--strip-enclosed-url" => val.flag_strip_enclosed_url = true,
127                    // Flavors
128                    "--flavor-hugging-face-wechat" => val.set_flavor_hugging_face_wechat(),
129                    // Debug
130                    "--debug-markdown-tag-pair" => val.debug_markdown_tag_pair = true,
131
132                    _ => {
133                        if arg.starts_with("-") {
134                            panic!("无法识别的选项 `{}`", arg);
135                        } else {
136                            should_explain = false;
137                        }
138                    }
139                }
140
141                if should_explain {
142                    options.push(arg.clone());
143                }
144            }
145            Some(val)
146        } else {
147            None
148        }
149    }
150
151    /// 使用 Hugging Face 微信公众号的风格
152    pub fn set_flavor_hugging_face_wechat(&mut self) {
153        self.flag_colon = true;
154        self.flag_enclose_table = true;
155        self.flag_semicolon = true;
156        self.flag_parenthesis = true;
157        self.flag_whitespace_around_bold_font = true;
158        self.flag_italic_to_bold_font = false; // Updated since 20230425
159        self.flag_extract_links = false; // Updated since 20230417
160        self.flag_extract_links_without_anchor = false; // Updated since 20230417
161        self.flag_strip_enclosed_url = true;
162        self.flag_google_list_style = false;
163    }
164
165    /// 打印所有风格选项
166    pub fn print_config(&self) {
167        println!("渲染选项启用状态");
168        let mut results: Vec<(&str, bool)> = vec![];
169        let mut max_width = 0;
170        results.push((
171            "Colon conversion",
172            if self.flag_colon { true } else { false },
173        ));
174        results.push((
175            "Extract links",
176            if self.flag_extract_links { true } else { false },
177        ));
178        results.push((
179            "Extract links without anchor",
180            if self.flag_extract_links_without_anchor {
181                true
182            } else {
183                false
184            },
185        ));
186        results.push((
187            "Semicolon conversion",
188            if self.flag_semicolon { true } else { false },
189        ));
190        results.push((
191            "List using Google style",
192            if self.flag_google_list_style {
193                true
194            } else {
195                false
196            },
197        ));
198        results.push((
199            "Parenthesis conversion",
200            if self.flag_parenthesis { true } else { false },
201        ));
202        results.push((
203            "Prettify table",
204            if self.flag_pretty_table { true } else { false },
205        ));
206
207        results.push((
208            "Italic to bold font conversion",
209            if self.flag_italic_to_bold_font {
210                true
211            } else {
212                false
213            },
214        ));
215        results.push((
216            "Whitespace around bold font",
217            if self.flag_whitespace_around_bold_font {
218                true
219            } else {
220                false
221            },
222        ));
223        results.push((
224            "Strip whitespace around enclosed URL",
225            if self.flag_strip_enclosed_url {
226                true
227            } else {
228                false
229            },
230        ));
231        results.sort_by(|item_a, item_b| {
232            max_width = max(max_width, max(item_a.0.len(), item_b.0.len()));
233            if item_a.1 == item_b.1 {
234                item_a.0.cmp(&item_b.0)
235            } else {
236                if item_a.1 {
237                    std::cmp::Ordering::Less
238                } else {
239                    std::cmp::Ordering::Greater
240                }
241            }
242        });
243        let mut counter: usize = 1;
244        let digits = if results.len() < 10 {
245            1
246        } else if results.len() < 100 {
247            2
248        } else if results.len() < 1000 {
249            3
250        } else {
251            4
252        };
253        println!(
254            "┌─{}──{}┬───┐",
255            "─".repeat(digits),
256            "─".repeat(max_width + 1)
257        );
258        results.iter().for_each(|res| {
259            if !self.compact_output && counter > 1 {
260                println!(
261                    "├─{}──{}┼───┤",
262                    "─".repeat(digits),
263                    "─".repeat(max_width + 1)
264                );
265            }
266            println!(
267                "│ {:digits$}. {:max_width$} │ {} │",
268                counter,
269                res.0,
270                if res.1 { "Y" } else { "N" }
271            );
272            counter += 1;
273        });
274        println!(
275            "└─{}──{}┴───┘",
276            "─".repeat(digits),
277            "─".repeat(max_width + 1)
278        );
279    }
280}
281
282fn print_help_info(
283    long_help: Option<bool>,
284    filtered: Option<Vec<String>>,
285) -> Vec<CommandLineOption> {
286    println!("命令用法: md-fmt [选项] <Markdown 文件名>\n");
287
288    let mut commands = vec![];
289
290    // 标准选项
291    commands.push(
292        CommandLineOption::new("--help", "输出当前的帮助信息 (--help 更详细)", "")
293            .set_condition(builder::CommandLineOptionCondition::Standard)
294            .set_short("-h"),
295    );
296
297    // 格式化选项
298    commands.push(
299        CommandLineOption::new(
300            "--colon",
301            "中文冒号转为英文冒号+空格",
302            "比如: “:” => “: ”。如果在行末,则会生成“:”,即删除末尾的空格。",
303        )
304        .set_condition(builder::CommandLineOptionCondition::Format),
305    );
306    commands.push(
307        CommandLineOption::new(
308            "--enclose-table",
309            "表格的每一行两端使用 |",
310            "比如表格的对齐行: “:-- | :--” => “| :-- | :-- |”。",
311        )
312        .set_condition(builder::CommandLineOptionCondition::Format),
313    );
314    commands.push(
315        CommandLineOption::new("--extract-links", "从段落中提取超链接", "比如:\n“这句话包含了一个[超链接](https://dongs.xyz/)” => “这句话包含了一个[超链接](https://dongs.xyz/)\\n\\n超链接:\\n<url>https://dongs.xyz/</url>”")
316            .set_condition(builder::CommandLineOptionCondition::Format),
317    );
318    commands.push(
319        CommandLineOption::new("--extract-no-anchor", "提取链接时不包括锚点部分", "")
320            .set_condition(builder::CommandLineOptionCondition::Format),
321    );
322    commands.push(
323        CommandLineOption::new("--google-list-style", "使用 Google 风格的列表样式", "")
324            .set_condition(builder::CommandLineOptionCondition::Format),
325    );
326    commands.push(
327        CommandLineOption::new(
328            "--italic-to-bold",
329            "斜体字更改为加粗文字",
330            "比如: “a*b*c” => “a**b**c”",
331        )
332        .set_condition(builder::CommandLineOptionCondition::Format),
333    );
334    commands.push(
335        CommandLineOption::new(
336            "--parenthesis",
337            "中文括号转为英文括号+空格",
338            "比如:\n“()” => “ () ”\n最终结果将根据上下文去除多余的空格。",
339        )
340        .set_condition(builder::CommandLineOptionCondition::Format),
341    );
342    commands.push(
343        CommandLineOption::new("--pretty-table", "以相同列宽展示表格中的每一列", "")
344            .set_condition(builder::CommandLineOptionCondition::Format),
345    );
346    commands.push(
347        CommandLineOption::new(
348            "--semicolon",
349            "中文分号转为英文分号+空格",
350            "比如: “;” => “; ”\n最终结果将根据上下文去除多余的空格。",
351        )
352        .set_condition(builder::CommandLineOptionCondition::Format),
353    );
354    commands.push(
355        CommandLineOption::new(
356            "--strip-enclosed-url",
357            "去除 <url> 内部的非必要空格",
358            "比如: “<url> https://dongs.xyz/ </url>” => “<url>https://dongs.xyz/</url>”",
359        )
360        .set_condition(builder::CommandLineOptionCondition::Format),
361    );
362    commands.push(
363        CommandLineOption::new(
364            "--whitespace-around-bold",
365            "加粗文字前后使用空格",
366            "比如:\n“a**b**c” => “a **b** c”;\n“a **b**c” => “a **b** c”。\n最终结果将根据上下文去除多余的空格。",
367        )
368        .set_condition(builder::CommandLineOptionCondition::Format),
369    );
370
371    // 附加选项
372    commands.push(
373        CommandLineOption::new("--print", "在控制台输出当前启用的调整", "")
374            .set_condition(builder::CommandLineOptionCondition::Additional),
375    );
376    commands.push(
377        CommandLineOption::new("--print-compact", "窄行距输出当前启用的调整", "")
378            .set_condition(builder::CommandLineOptionCondition::Additional),
379    );
380
381    // 调试选项
382    commands.push(
383        CommandLineOption::new(
384            "--debug-markdown-tag-pair",
385            "执行 Markdown 模块的函数时输出成对的 Tag (仅适用于 `debug` feature)",
386            "",
387        )
388        .set_condition(builder::CommandLineOptionCondition::Debug),
389    );
390
391    // 定制风格
392    commands.push(
393        CommandLineOption::new(
394            "--flavor-hugging-face-wechat",
395            "使用 Hugging Face 微信公众号风格来调整输入内容",
396            "",
397        )
398        .set_condition(builder::CommandLineOptionCondition::Flavor),
399    );
400
401    // use termsize::{get as get_termsize, Size};
402    // Switch to termsize-alt because termsize crates on crates.io is out dated.
403    use termsize_alt::{get as get_termsize, Size};
404    let Size { rows: _, cols } = get_termsize().unwrap_or(Size {
405        rows: 40,
406        cols: 120,
407    });
408
409    let line_width = Some(cols.into());
410
411    match long_help {
412        Some(true) => print_doc(commands.clone(), filtered, line_width),
413        _ => print_help(commands.clone(), line_width),
414    }
415
416    commands
417}
418
419#[cfg(test)]
420mod test {
421    use crate::args::Args;
422    #[test]
423    fn test_argument_parsing() {
424        let mut options: Vec<String> = vec!["--colon", "--parenthesis", "test.md"]
425            .iter()
426            .map(|s| s.to_string())
427            .collect();
428        if let Some(args) = Args::parse(options) {
429            assert_eq!(args.flag_colon, true);
430            assert_eq!(args.flag_parenthesis, true);
431        };
432
433        options = vec!["--flavor-hugging-face-wechat", "test.md"]
434            .iter()
435            .map(|s| s.to_string())
436            .collect();
437        if let Some(args) = Args::parse(options) {
438            assert_eq!(args.flag_colon, true);
439            assert_eq!(args.flag_parenthesis, true);
440            assert_eq!(args.flag_italic_to_bold_font, true);
441            assert_eq!(args.flag_whitespace_around_bold_font, true);
442        };
443    }
444
445    #[test]
446    fn test_malformed_options() {
447        let options = vec!["--flavor-hugging-face-wechat"]
448            .iter()
449            .map(|s| s.to_string())
450            .collect();
451
452        Args::parse(options);
453    }
454}