Skip to main content

hugox/
add_tag.rs

1use std::{
2    collections::BTreeSet,
3    ffi::OsString,
4    io::{BufReader, Error, ErrorKind, Result},
5    path::PathBuf,
6};
7
8use clap::Args;
9use regex::Regex;
10
11use crate::{file_utils, RunCommand};
12
13static TAG_LINE_PATTERN: &str = r"^\s*tags\s*:\s*[ ]*\[.*\]\s*";
14
15/// Subcommand `add_tag`
16#[derive(Args, Debug)]
17pub struct AddTag {
18    /// Tags to be added.
19    #[clap(required = true)]
20    tags: Vec<String>,
21    /// Sets a custom config file
22    #[clap(short, long, parse(from_os_str), value_name = "FILE")]
23    path: PathBuf,
24}
25
26/// Implements the `RunCommand` trait.
27impl RunCommand for AddTag {
28    fn run(&self) {
29        self.add_tag()
30    }
31}
32
33/// Implements the command of add-tag
34impl AddTag {
35    /// Entry point of the command add_tag.
36    fn add_tag(&self) {
37        let Self { tags, path } = self;
38
39        // get files
40        let files = file_utils::list_all_files(path);
41        // add tags
42        for file in files {
43            println!("Updating file : {}", file.as_os_str().to_str().unwrap());
44            do_add_tag(&tags, &file);
45        }
46        println!("Complete.");
47    }
48}
49
50/// Add tags to file.
51pub fn do_add_tag(new_tags: &Vec<String>, file: &OsString) {
52    let (line, start, end) = extract_tag_line(&file).unwrap();
53    let new_line = extend_tag(line, new_tags);
54    file_utils::replace_in_file(&file, start, end, new_line).unwrap();
55}
56
57/// Extend tag line with new tags.
58///
59/// # Examples
60///
61/// ```text
62///     use hugox::add_tag::extend_tag;
63///     let line = "tags: [a, b, c]";
64///     let new_tags = vec!["d".to_string(), "e".to_string()];
65///     let result = extend_tag(line.to_string(), &new_tags);
66///     assert_eq!(result, "tags: [a, b, c, d, e]");
67/// ```
68fn extend_tag(old_tag_line: String, new_tags: &Vec<String>) -> String {
69    let mut tags = old_tag_line
70        .split_once("tags")
71        .unwrap()
72        .1
73        .split_once(":")
74        .unwrap()
75        .1
76        .split_once("[")
77        .unwrap()
78        .1
79        .split_once("]")
80        .unwrap()
81        .0
82        .split(",")
83        .map(|s| s.trim().to_string())
84        .collect::<BTreeSet<String>>();
85    tags.extend(new_tags.iter().cloned());
86
87    format!(
88        "tags: [{}]",
89        tags.into_iter().collect::<Vec<String>>().join(", ")
90    )
91}
92
93/// Extract tag line from given file. As well as start and end position of the tag line.
94///
95/// For example, `note.md` included a frontmatter like this:
96///
97/// ```text
98/// // note.md
99/// ---
100/// title: "30天自制操作系统"
101/// tags: [学习笔记, 学习, 笔记]
102/// draft: false
103/// description: "30天自制操作系统"
104/// source: "http://hrb.osask.jp/"
105/// ---
106/// ```
107///
108/// ```no_run
109///     use hugox::add_tag::extract_tag_line;
110///     use std::ffi::OsString;
111///
112///     let (line, start, end) = extract_tag_line(&OsString::from("note.md")).unwrap();
113///     assert_eq!(line, "tags: [学习笔记, 学习, 笔记]\n");
114///     assert_eq!(start, 16);
115///     assert_eq!(end, 53);
116/// ```
117pub fn extract_tag_line(file: &OsString) -> Result<(String, usize, usize)> {
118    use std::io::BufRead;
119
120    use std::fs::OpenOptions;
121
122    let file = OpenOptions::new()
123        .read(true)
124        .write(true)
125        .create(true)
126        .open(file)
127        .unwrap();
128
129    let re = Regex::new(TAG_LINE_PATTERN).unwrap();
130
131    let mut reader = BufReader::new(file);
132    let (mut i, mut _j) = (0, 0);
133    loop {
134        let mut line = String::new();
135        match reader.read_line(&mut line) {
136            Ok(0) => return Err(Error::new(ErrorKind::Other, "No tag line found")),
137            Ok(size) => {
138                if re.is_match(&line) {
139                    _j = i + size;
140                    return Ok((String::from(line), i, _j));
141                } else {
142                    i += size;
143                }
144            }
145            Err(e) => {
146                eprintln!("{}", e);
147                return Err(e);
148            }
149        }
150    }
151}
152
153#[test]
154fn test_modify_tag() {
155    let line = "tags: [a, b, c]";
156    let new_tags = vec!["d".to_string(), "e".to_string()];
157    let result = extend_tag(line.to_string(), &new_tags);
158    assert_eq!(result, "tags: [a, b, c, d, e]");
159
160    let re = Regex::new(TAG_LINE_PATTERN).unwrap();
161    assert_eq!(re.is_match(line), true);
162}
163
164#[test]
165fn test_regex() {
166    let re = Regex::new(TAG_LINE_PATTERN).unwrap();
167    let line = "tags: [a, b, c]";
168    assert_eq!(re.is_match(line), true);
169    let line = "  tags : [ a ,  b,  c  ] ";
170    assert_eq!(re.is_match(line), true);
171}