#+property: tangle lib.rs
#+title: md-tangle-engine

* use

  #+begin_src rust
  use std::path::Path;
  use std::path::PathBuf;

  use std::str::Lines;

  use std::fs;
  use std::io;
  use std::env;
  #+end_src

* TangleError

  #+begin_src rust
  #[derive(Debug)]
  struct TangleError {
      report: String,
  }

  impl TangleError {
      fn new (report: &str) -> Self {
          TangleError {
              report: report.to_string (),
          }
      }
  }
  #+end_src

* find_meta_block

*** meta_block_line_p

    #+begin_src rust
    fn meta_block_line_p (line: &str) -> bool {
        if ! line .trim_start () .starts_with ("---") {
            false
        } else {
            let string = line.trim ();
            string .chars () .all (|x| x == '-')
        }
    }
    #+end_src

*** meta_block_collect

    #+begin_src rust
    fn meta_block_collect_string (
        lines: &mut Lines,
    ) -> Option <String> {
        let mut result = String::new ();
        for line in lines {
            if meta_block_line_p (line) {
                return Some (result);
            } else {
                result.push_str (line);
                result.push ('\n');
            }
        }
        None
    }
    #+end_src

*** find_meta_block

    #+begin_src rust
    fn find_meta_block (string: &str) -> Option <String> {
        let mut lines = string.lines ();
        if let Some (first_line) = lines.next () {
            if meta_block_line_p (first_line) {
                meta_block_collect_string (&mut lines)
            } else {
                None
            }
        } else {
            None
        }
    }
    #+end_src

* find_destination

  #+begin_src rust
  const DESTINATION_PREFIX: &'static str = "tangle:";

  fn destination_line_p (line: &str) -> bool {
      line .trim_start () .starts_with (DESTINATION_PREFIX)
  }

  fn find_destination (string: &str) -> Option <String> {
      let meta_block = find_meta_block (string);
      if meta_block.is_none () {
          return None;
      }
      let string = meta_block .unwrap ();
      for line in string.lines () {
          if destination_line_p (line) {
              let destination = &line [DESTINATION_PREFIX.len () ..];
              let destination = destination.trim ();
              return Some (destination.to_string ());
          }
      }
      None
  }

  #[test]
  fn test_find_destination () {
      let example = format! (
          "{}\n{}\n{}\n",
          "---",
          "tangle: core.rs",
          "---",
      );
      let destination = find_destination (&example) .unwrap ();
      assert_eq! (destination, "core.rs");
  }
  #+end_src

* tangle

*** line predicates

    #+begin_src rust
    const BLOCK_BEGIN: &'static str = "```";
    const BLOCK_END: &'static str = "```";

    fn block_begin_line_p (line: &str) -> bool {
        line .trim_start () .starts_with (BLOCK_BEGIN)
    }

    fn block_end_line_p (line: &str) -> bool {
        line .trim_start () .starts_with (BLOCK_END)
    }
    #+end_src

*** tangle_collect

    #+begin_src rust
    fn tangle_collect (
        result: &mut String,
        lines: &mut Lines,
    ) -> Result <(), TangleError> {
        for line in lines {
            if block_end_line_p (line) {
                return Ok (());
            } else {
                result.push_str (line);
                result.push ('\n');
            }
        }
        let error = TangleError::new ("block_end mismatch");
        Err (error)
    }
    #+end_src

*** tangle

    #+begin_src rust
    fn tangle (string: &str) -> Result <String, TangleError> {
        let mut result = String::new ();
        let mut lines = string.lines ();
        while let Some (line) = lines.next () {
            if block_begin_line_p (line) {
                tangle_collect (&mut result, &mut lines)?;
            }
        }
        Ok (result)
    }
    #+end_src

*** test_tangle

    #+begin_src rust
    #[test]
    fn test_tangle () {
        let example = format! (
            "{}\n{}\n{}\n{}\n",
            "``` rust",
            "hi",
            "hi",
            "```",
        );
        let expect = format! (
            "{}\n{}\n",
            "hi",
            "hi",
        );
        let result = tangle (&example) .unwrap ();
        assert_eq! (expect, result);
        let example = format! (
            "{}\n{}\n{}\n{}\n",
            "    ``` rust",
            "    hi",
            "    hi",
            "    ```",
        );
        let expect = format! (
            "{}\n{}\n",
            "    hi",
            "    hi",
        );
        let result = tangle (&example) .unwrap ();
        assert_eq! (expect, result);
    }
    #+end_src

* interface

*** good_path_p

    #+begin_src rust
    fn good_path_p (path: &Path) -> bool {
        for component in path.iter () {
            if let Some (string) = component.to_str () {
                if string.starts_with ('.') {
                    if ! string .chars () .all (|x| x == '.') {
                        return false;
                    }
                }
            } else {
                return false;
            }
        }
        true
    }
    #+end_src

*** md_file_p

    #+begin_src rust
    pub fn md_file_p (file: &Path) -> bool {
        if let Some (os_string) = file.extension () {
            if let Some (string) = os_string.to_str () {
                string == "md"
            } else {
                false
            }
        } else {
            false
        }
    }
    #+end_src

*** file_tangle

    #+begin_src rust
    pub fn file_tangle (file: &Path) -> io::Result <()> {
        if ! md_file_p (file) {
            return Ok (());
        }
        println! ("- tangle : {:?}", file);
        let string = fs::read_to_string (file)?;
        if let Some (destination) = find_destination (&string) {
            let result = tangle (&string) .unwrap ();
            let mut destination_path = PathBuf::new ();
            destination_path.push (file);
            destination_path.pop ();
            destination_path.push (destination);
            fs::write (&destination_path, result)
        } else {
            Ok (())
        }
    }
    #+end_src

*** dir_tangle

    #+begin_src rust
    pub fn dir_tangle (dir: &Path) -> io::Result <()> {
        for entry in dir.read_dir ()? {
            if let Ok (entry) = entry {
                if good_path_p (&entry.path ()) {
                    if entry.file_type ()? .is_file () {
                        file_tangle (&entry.path ())?
                    }
                }
            }
        }
        Ok (())
    }
    #+end_src

*** dir_tangle_rec

    #+begin_src rust
    pub fn dir_tangle_rec (dir: &Path) -> io::Result <()> {
        for entry in dir.read_dir ()? {
            if let Ok (entry) = entry {
                if good_path_p (&entry.path ()) {
                    if entry.file_type ()? .is_file () {
                        file_tangle (&entry.path ())?
                    } else if entry.file_type ()? .is_dir () {
                        dir_tangle_rec (&entry.path ())?
                    }
                }
            }
        }
        Ok (())
    }
    #+end_src

*** absolute_lize

    #+begin_src rust
    pub fn absolute_lize (path: &Path) -> PathBuf {
        if path.is_relative () {
            let mut absolute_path = env::current_dir () .unwrap ();
            absolute_path.push (path);
            absolute_path
        } else {
            path.to_path_buf ()
        }
    }
    #+end_src

*** tangle_all_before_build

    #+begin_src rust
    pub fn tangle_all_before_build () -> io::Result <()> {
        let path = Path::new (".");
        let current_dir = env::current_dir () .unwrap ();
        println! ("- md_tangle_engine");
        println! ("  tangle_all_before_build");
        println! ("  current_dir : {:?}", current_dir);
        let path = absolute_lize (&path);
        dir_tangle_rec (&path)
    }
    #+end_src
