agda_tree/command/
build.rs

1use html_parser::{Dom, Element, Node};
2use std::collections::VecDeque;
3use std::fs::{self, create_dir_all, read_to_string, File};
4use std::io::{self, Write};
5use std::iter::zip;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9use crate::extract::extract_agda_code;
10use crate::tree::Tree;
11
12pub fn execute(working_dir: &PathBuf, output_dir: &PathBuf, skip_agda: bool) -> io::Result<()> {
13    let paths = fs::read_dir(working_dir)?
14        .filter_map(Result::ok)
15        .filter_map(|f| {
16            if let Ok(ft) = f.file_type() {
17                if ft.is_file() && f.path().to_str()?.ends_with(".lagda.tree") {
18                    return Some(f.path());
19                }
20            }
21            None
22        })
23        .collect::<Vec<PathBuf>>();
24
25    let trees = generate_lagda_md(&paths)?;
26    let index_path = generate_index(working_dir, &paths)?;
27    if !skip_agda {
28        run_agda_build(working_dir, index_path)?;
29    }
30    collect_html(working_dir, output_dir, &paths, trees)
31}
32
33fn generate_lagda_md(paths: &Vec<PathBuf>) -> io::Result<Vec<Tree>> {
34    let mut r = vec![];
35    for path in paths {
36        let (tree, agda_blocks) = extract_agda_code(&path)?;
37        let mut middle = File::create(path.with_extension("md"))?;
38        for block in agda_blocks {
39            middle.write(block.as_bytes())?;
40        }
41        r.push(tree);
42    }
43    Ok(r)
44}
45
46fn run_agda_build(working_dir: &PathBuf, index_path: PathBuf) -> io::Result<()> {
47    let _ = Command::new("agda")
48        .current_dir(working_dir)
49        .args([
50            "--html",
51            index_path.into_os_string().into_string().unwrap().as_str(),
52        ])
53        .output()
54        .expect("failed to build agda htmls");
55    Ok(())
56}
57
58fn generate_index(working_dir: &PathBuf, paths: &Vec<PathBuf>) -> io::Result<PathBuf> {
59    // generate a index agda module, import our `.lagda.md`
60    let imports = paths
61        .into_iter()
62        .map(|path| format!("import {}", path.file_prefix().unwrap().to_str().unwrap()))
63        .collect::<Vec<String>>();
64    let index_path = working_dir.join("index.agda");
65    let mut index = File::create(&index_path)?;
66    for imp in imports {
67        index.write(imp.as_bytes())?;
68    }
69    Ok(index_path)
70}
71
72fn collect_html(
73    working_dir: &PathBuf,
74    output_dir: &PathBuf,
75    paths: &Vec<PathBuf>,
76    trees: Vec<Tree>,
77) -> io::Result<()> {
78    for (path, tree) in zip(paths.into_iter(), trees.into_iter()) {
79        let basename = path.file_prefix().unwrap().to_str().unwrap();
80        let agda_html = working_dir
81            .join("html")
82            .join(basename)
83            .with_extension("html");
84
85        let s = read_to_string(&agda_html)
86            .expect(format!("failed to open generated html file `{:?}`", agda_html).as_str());
87        let dom = Dom::parse(s.as_str()).unwrap();
88
89        let nodes = &dom.children[0].element().unwrap().children[1]
90            .element()
91            .unwrap()
92            .children[0]
93            .element()
94            .unwrap()
95            .children;
96
97        let forester_blocks = agda_html_blocks(working_dir, nodes);
98
99        let new_tree = tree.merge(forester_blocks);
100
101        create_dir_all(output_dir)?;
102        let output = File::create(output_dir.join(basename).with_extension("tree"))?;
103        new_tree.write(output);
104    }
105
106    Ok(())
107}
108
109fn agda_html_blocks(working_dir: &PathBuf, nodes: &Vec<Node>) -> VecDeque<String> {
110    let mut blocks = VecDeque::new();
111    let mut buffer = String::new();
112    let mut recording = false;
113    let mut line = line_of_symbol(nodes[0].element().unwrap());
114    let mut last_col_end = end_col_of_symbol(nodes[0].element().unwrap());
115
116    for node in nodes {
117        let elem = node.element().unwrap();
118        if is_block_start(elem) {
119            recording = true;
120            line = line_of_symbol(elem);
121            last_col_end = col_of_symbol(elem);
122            buffer.push_str("\\<html:pre>[class]{Agda}{\n");
123        } else if is_block_end(elem) {
124            buffer.push_str("}");
125            blocks.push_back(buffer);
126            recording = false;
127            buffer = String::new();
128        } else if recording {
129            if line_of_symbol(elem) > line {
130                for _ in 0..(line_of_symbol(elem) - line) {
131                    buffer.push('\n');
132                }
133                last_col_end = 1;
134            }
135            if col_of_symbol(elem) > last_col_end {
136                for _ in 0..col_of_symbol(elem) - last_col_end {
137                    buffer.push(' ');
138                }
139            }
140            last_col_end = end_col_of_symbol(elem);
141            line = line_of_symbol(elem);
142            buffer.push_str(symbol2forest(working_dir, elem).as_str());
143        }
144    }
145
146    blocks
147}
148
149fn is_block_start(elem: &Element) -> bool {
150    !elem.children.is_empty() && elem.children[0].text().unwrap().contains("```agda")
151}
152fn is_block_end(elem: &Element) -> bool {
153    !elem.children.is_empty() && elem.children[0].text().unwrap().contains("```")
154}
155
156fn line_of_symbol(elem: &Element) -> usize {
157    elem.source_span.end_line
158}
159fn col_of_symbol(elem: &Element) -> usize {
160    elem.source_span.start_column
161}
162fn end_col_of_symbol(elem: &Element) -> usize {
163    elem.source_span.end_column
164}
165
166fn symbol2forest(working_dir: &PathBuf, elem: &Element) -> String {
167    let mut s = format!("\\<html:{}>", elem.name);
168
169    if elem.id.is_some() {
170        s.push_str(format!("[id]{{{}}}", elem.id.clone().unwrap().as_str()).as_str());
171    }
172    if !elem.classes.is_empty() {
173        s.push_str(format!("[class]{{{}}}", elem.classes[0]).as_str());
174    }
175    for (k, v) in &elem.attributes {
176        let value = v.clone().unwrap();
177        let value = if k == "href" {
178            // value is a xxx.html#id
179            // 1. split at `#`
180            // 2. if there is a `xxx.lagda.tree` in workding dir, replace the path with `xxx.xml`
181            // 3. put `#id` back if exists
182            let split = value.split_terminator('#').collect::<Vec<&str>>();
183            let a_link = split[0];
184            let path = Path::new(a_link);
185            if working_dir.join(path).with_extension("lagda.tree").exists() {
186                let mut s = path.with_extension("xml").to_str().unwrap().to_owned();
187                s.push('#');
188                if split.len() == 2 {
189                    let id_part = split[1];
190                    s.push_str(id_part);
191                }
192                s
193            } else {
194                value
195            }
196        } else {
197            value
198        };
199        s.push_str(format!("[{}]{{{}}}", k, value).as_str());
200    }
201    if elem.children.is_empty() {
202        s.push_str("{}");
203    } else {
204        let childtext = elem.children[0].text().unwrap();
205        // some escape code is useful for HTML, but not for forester
206        let childtext = if childtext.contains("&#39;") {
207            childtext.replace("&#39;", "'")
208        } else {
209            childtext.to_owned()
210        };
211        if childtext.contains('(')
212            || childtext.contains(')')
213            || childtext.contains('{')
214            || childtext.contains('}')
215        {
216            s.push_str(format!("{{\\startverb{}\\stopverb}}", childtext).as_str());
217        } else {
218            s.push_str(format!("{{{}}}", childtext).as_str());
219        }
220    }
221
222    s
223}