org_tangle_engine/
lib.rs

1use std::path::Path;
2use std::path::PathBuf;
3
4use std::str::Lines;
5
6use std::fs;
7use std::io;
8use std::env;
9
10#[derive(Debug)]
11struct TangleError {
12    report: String,
13}
14
15impl TangleError {
16    fn new (report: &str) -> Self {
17        TangleError {
18            report: report.to_string (),
19        }
20    }
21}
22
23fn property_line_p (line: &str) -> bool {
24    line .trim_start () .starts_with ("#+property:")
25}
26
27fn find_destination_in_property_line (
28    line: &str,
29) -> Option <String> {
30    let mut words = line.split_whitespace ();
31    while let Some (word) = words.next () {
32        if word == "tangle" || word == ":tangle" {
33            if let Some (destination) = words.next () {
34                return Some (destination.to_string ())
35            }
36        }
37    }
38    None
39}
40
41fn find_destination (string: &str) -> Option <String> {
42    for line in string.lines () {
43        if property_line_p (line) {
44            let destination =
45                find_destination_in_property_line (line);
46            if destination. is_some () {
47                return destination;
48            }
49        }
50    }
51    None
52}
53
54#[test]
55fn test_find_destination () {
56    let example = "#+property: tangle lib.rs";
57    let destination = find_destination (example) .unwrap ();
58    assert_eq! (destination, "lib.rs");
59
60    let example = "#+property: header-args :tangle lib.rs";
61    let destination = find_destination (example) .unwrap ();
62    assert_eq! (destination, "lib.rs");
63}
64
65fn block_begin_line_p (line: &str) -> bool {
66    line .trim_start () .starts_with ("#+begin_src")
67}
68
69fn block_end_line_p (line: &str) -> bool {
70    line .trim_start () .starts_with ("#+end_src")
71}
72
73fn block_indentation (line: &str) -> usize {
74    let mut indentation = 0;
75    for ch in line.chars () {
76        if ch == ' ' {
77            indentation += 1;
78        } else {
79            return indentation;
80        }
81    }
82    0
83}
84
85    fn line_trim_indentation <'a> (
86        mut line: &'a str,
87        indentation: usize,
88    ) -> &'a str {
89        let mut counter = 0;
90        while counter < indentation {
91            if line.starts_with (' ') {
92                counter += 1;
93                line = &line[1..];
94            } else {
95                return line;
96            }
97        }
98        line
99    }
100
101fn tangle_collect (
102    result: &mut String,
103    lines: &mut Lines,
104    indentation: usize,
105) -> Result <(), TangleError> {
106    for line in lines {
107        if block_end_line_p (line) {
108            result.push ('\n');
109            return Ok (());
110        } else {
111            let line = line_trim_indentation (
112                line, indentation);
113            result.push_str (line);
114            result.push ('\n');
115        }
116    }
117    let error = TangleError::new ("block_end mismatch");
118    Err (error)
119}
120
121fn tangle (string: &str) -> Result <String, TangleError> {
122    let mut result = String::new ();
123    let mut lines = string.lines ();
124    while let Some (line) = lines.next () {
125        if block_begin_line_p (line) {
126            tangle_collect (
127                &mut result,
128                &mut lines,
129                block_indentation (line))?;
130        }
131    }
132    result.pop ();
133    Ok (result)
134}
135
136#[test]
137fn test_tangle () {
138    let example = format! (
139        "{}\n{}\n{}\n{}\n",
140        "#+begin_src rust",
141        "hi",
142        "hi",
143        "#+end_src",
144    );
145    let expect = format! (
146        "{}\n{}\n",
147        "hi",
148        "hi",
149    );
150    let result = tangle (&example) .unwrap ();
151    assert_eq! (expect, result);
152
153    let example = format! (
154        "{}\n{}\n{}\n{}\n",
155        "    #+begin_src rust",
156        "    hi",
157        "    hi",
158        "    #+end_src",
159    );
160    let expect = format! (
161        "{}\n{}\n",
162        "hi",
163        "hi",
164    );
165    let result = tangle (&example) .unwrap ();
166    assert_eq! (expect, result);
167
168    let example = format! (
169        "{}\n{}\n{}\n{}\n",
170        "#+begin_src rust",
171        "    hi",
172        "    hi",
173        "#+end_src",
174    );
175    let expect = format! (
176        "{}\n{}\n",
177        "    hi",
178        "    hi",
179    );
180    let result = tangle (&example) .unwrap ();
181    assert_eq! (expect, result);
182}
183
184fn good_path_p (path: &Path) -> bool {
185    for component in path.iter () {
186        if let Some (string) = component.to_str () {
187            if string.starts_with ('.') {
188                if ! string .chars () .all (|x| x == '.') {
189                    return false;
190                }
191            }
192        } else {
193            return false;
194        }
195    }
196    true
197}
198
199pub fn org_file_p (file: &Path) -> bool {
200    if let Some (os_string) = file.extension () {
201        if let Some (string) = os_string.to_str () {
202            string == "org"
203        } else {
204            false
205        }
206    } else {
207        false
208    }
209}
210
211pub fn file_tangle (file: &Path) -> io::Result <()> {
212    if ! org_file_p (file) {
213        return Ok (());
214    }
215    let string = fs::read_to_string (file)?;
216    if let Some (destination) = find_destination (&string) {
217        let result = tangle (&string) .unwrap ();
218        let mut destination_path = PathBuf::new ();
219        destination_path.push (file);
220        destination_path.pop ();
221        destination_path.push (destination);
222        fs::write (&destination_path, result)?;
223        println! (
224            "- tangle : {:?} => {:?}",
225            file.canonicalize ()?,
226            destination_path.canonicalize ()?);
227        Ok (())
228    } else {
229        Ok (())
230    }
231}
232
233pub fn dir_tangle (dir: &Path) -> io::Result <()> {
234    for entry in dir.read_dir ()? {
235        if let Ok (entry) = entry {
236            if good_path_p (&entry.path ()) {
237                if entry.file_type ()? .is_file () {
238                    file_tangle (&entry.path ())?
239                }
240            }
241        }
242    }
243    Ok (())
244}
245
246pub fn dir_tangle_rec (dir: &Path) -> io::Result <()> {
247    for entry in dir.read_dir ()? {
248        if let Ok (entry) = entry {
249            if good_path_p (&entry.path ()) {
250                if entry.file_type ()? .is_file () {
251                    file_tangle (&entry.path ())?
252                } else if entry.file_type ()? .is_dir () {
253                    dir_tangle_rec (&entry.path ())?
254                }
255            }
256        }
257    }
258    Ok (())
259}
260
261pub fn absolute_lize (path: &Path) -> PathBuf {
262    if path.is_relative () {
263        let mut absolute_path = env::current_dir () .unwrap ();
264        absolute_path.push (path);
265        absolute_path
266    } else {
267        path.to_path_buf ()
268    }
269}
270
271pub fn tangle_all_before_build () -> io::Result <()> {
272    let path = Path::new (".");
273    let current_dir = env::current_dir () .unwrap ();
274    println! ("- org_tangle_engine");
275    println! ("  tangle_all_before_build");
276    println! ("  current_dir : {:?}", current_dir);
277    let path = absolute_lize (&path);
278    dir_tangle_rec (&path)
279}