Cached building

This commit is contained in:
ef3d0c3e 2024-07-29 13:32:05 +02:00
parent ac0c4050eb
commit c8d35a7dc3
3 changed files with 250 additions and 136 deletions

View file

@ -9,9 +9,6 @@ use crate::document::document::Document;
use crate::document::document::ElemReference;
use crate::document::variable::Variable;
use super::navigation::NavEntry;
use super::navigation::Navigation;
#[derive(Clone, Copy)]
pub enum Target {
HTML,
@ -133,80 +130,128 @@ impl Compiler {
result
}
pub fn navigation(&self, navigation: &Navigation, document: &dyn Document) -> String
{
let mut result = String::new();
match self.target()
{
Target::HTML => {
result += r#"<ul id="navbar">"#;
fn process(result: &mut String, name: &String, ent: &NavEntry, depth: usize)
{
let ent_path = ent.path.as_ref()
.map_or("#".to_string(),|path| path.clone());
result.push_str(format!(r#"<li><a href="{ent_path}">{name}</a></li>"#).as_str());
if let Some(children) = ent.children.as_ref()
{
result.push_str("<ul>");
for (name, ent) in children
{
process(result, name, ent, depth+1);
}
result.push_str("</ul>");
}
}
for (name, ent) in &navigation.entries
{
process(&mut result, name, ent, 0);
}
result += r#"</ul>"#;
},
_ => todo!("")
}
result
}
pub fn footer(&self, _document: &dyn Document) -> String {
let mut result = String::new();
match self.target() {
Target::HTML => {
result += "</body></html>";
}
Target::LATEX => todo!("")
Target::LATEX => todo!(""),
}
result
}
pub fn compile(&self, navigation: &Navigation, document: &dyn Document) -> String {
let mut out = String::new();
pub fn compile(&self, document: &dyn Document) -> CompiledDocument {
let borrow = document.content().borrow();
// Header
out += self.header(document).as_str();
// Navigation
out += self.navigation(navigation, document).as_str();
let header = self.header(document);
// Body
let mut body = String::new();
for i in 0..borrow.len() {
let elem = &borrow[i];
match elem.compile(self, document) {
Ok(result) => {
out.push_str(result.as_str())
}
Ok(result) => body.push_str(result.as_str()),
Err(err) => println!("Unable to compile element: {err}\n{}", elem.to_string()),
}
}
// Footer
out += self.footer(document).as_str();
let footer = self.footer(document);
out
// Variables
let variables = document
.scope()
.borrow_mut()
.variables
.iter()
.map(|(key, var)| (key.clone(), var.to_string()))
.collect::<HashMap<String, String>>();
CompiledDocument {
input: document.source().name().clone(),
mtime: 0,
variables,
header,
body,
footer,
}
}
}
#[derive(Debug)]
pub struct CompiledDocument {
/// Input path relative to the input directory
pub input: String,
/// Modification time (i.e seconds since last epoch)
pub mtime: u64,
// TODO: Also store exported references
// so they can be referenced from elsewhere
// This will also require rebuilding in case some exported references have changed...
/// Variables exported to string, so they can be querried later
pub variables: HashMap<String, String>,
/// Compiled document's header
pub header: String,
/// Compiled document's body
pub body: String,
/// Compiled document's footer
pub footer: String,
}
impl CompiledDocument {
pub fn get_variable(&self, name: &str) -> Option<&String> { self.variables.get(name) }
fn sql_table() -> &'static str {
"CREATE TABLE IF NOT EXISTS compiled_documents (
input TEXT PRIMARY KEY,
mtime INTEGER NOT NULL,
variables TEXT NOT NULL,
header TEXT NOT NULL,
body TEXT NOT NULL,
footer TEXT NOT NULL
);"
}
fn sql_get_query() -> &'static str { "SELECT * FROM compiled_documents WHERE input = (?1)" }
fn sql_insert_query() -> &'static str {
"INSERT OR REPLACE INTO compiled_documents (input, mtime, variables, header, body, footer) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
}
pub fn init_cache(con: &Connection) -> Result<usize, rusqlite::Error> {
con.execute(Self::sql_table(), [])
}
pub fn from_cache(con: &Connection, input: &String) -> Option<Self> {
con.query_row(Self::sql_get_query(), [input], |row| {
Ok(CompiledDocument {
input: input.clone(),
mtime: row.get_unwrap::<_, u64>(1),
variables: serde_json::from_str(row.get_unwrap::<_, String>(2).as_str()).unwrap(),
header: row.get_unwrap::<_, String>(3),
body: row.get_unwrap::<_, String>(4),
footer: row.get_unwrap::<_, String>(5),
})
})
.ok()
}
/// Inserts [`CompiledDocument`] into cache
pub fn insert_cache(&self, con: &Connection) -> Result<usize, rusqlite::Error> {
con.execute(
Self::sql_insert_query(),
(
&self.input,
&self.mtime,
serde_json::to_string(&self.variables).unwrap(),
&self.header,
&self.body,
&self.footer,
),
)
}
}

View file

@ -1,6 +1,7 @@
use std::collections::HashMap;
use crate::document::document::Document;
use super::compiler::CompiledDocument;
use super::compiler::Target;
#[derive(Debug)]
pub struct NavEntry {
@ -14,7 +15,43 @@ pub struct Navigation {
pub(crate) entries: HashMap<String, NavEntry>,
}
pub fn create_navigation(docs: &Vec<Box<dyn Document>>) -> Result<Navigation, String> {
impl Navigation {
pub fn compile(&self, target: Target) -> String {
let mut result = String::new();
match target {
Target::HTML => {
result += r#"<ul id="navbar">"#;
fn process(result: &mut String, name: &String, ent: &NavEntry, depth: usize) {
let ent_path = ent
.path
.as_ref()
.map_or("#".to_string(), |path| path.clone());
result
.push_str(format!(r#"<li><a href="{ent_path}">{name}</a></li>"#).as_str());
if let Some(children) = ent.children.as_ref() {
result.push_str("<ul>");
for (name, ent) in children {
process(result, name, ent, depth + 1);
}
result.push_str("</ul>");
}
}
for (name, ent) in &self.entries {
process(&mut result, name, ent, 0);
}
result += r#"</ul>"#;
}
_ => todo!(""),
}
result
}
}
pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<Navigation, String> {
let mut nav = Navigation {
entries: HashMap::new(),
};
@ -30,63 +67,57 @@ pub fn create_navigation(docs: &Vec<Box<dyn Document>>) -> Result<Navigation, St
let (cat, title, path) = match (cat, title, path) {
(Some(cat), Some(title), Some(path)) => (cat, title, path),
_ => {
println!(
"Skipping navigation generation for `{}`",
doc.source().name()
);
println!("Skipping navigation generation for `{}`", doc.input);
continue;
}
};
if let Some(subcat) = subcat {
// Get parent entry
let cat_name = cat.to_string();
let mut pent = match nav.entries.get_mut(cat_name.as_str()) {
let mut pent = match nav.entries.get_mut(cat.as_str()) {
Some(pent) => pent,
None => {
// Create parent entry
nav.entries.insert(
cat_name.clone(),
cat.clone(),
NavEntry {
name: cat_name.clone(),
name: cat.clone(),
path: None,
children: Some(HashMap::new()),
},
);
nav.entries.get_mut(cat_name.as_str()).unwrap()
nav.entries.get_mut(cat.as_str()).unwrap()
}
};
// Insert into parent
let subcat_name = subcat.to_string();
if let Some(previous) = pent.children.as_mut().unwrap().insert(
subcat_name.clone(),
subcat.clone(),
NavEntry {
name: subcat_name.clone(),
name: subcat.clone(),
path: Some(path.to_string()),
children: None,
},
) {
return Err(format!(
"Duplicate subcategory:\n{subcat:#?}\nclashes with:\n{previous:#?}"
"Duplicate subcategory:\n{subcat}\nclashes with:\n{previous:#?}"
));
}
} else {
// Get entry
let cat_name = cat.to_string();
let mut ent = match nav.entries.get_mut(cat_name.as_str()) {
let mut ent = match nav.entries.get_mut(cat.as_str()) {
Some(ent) => ent,
None => {
// Create parent entry
nav.entries.insert(
cat_name.clone(),
cat.clone(),
NavEntry {
name: cat_name.clone(),
name: cat.clone(),
path: None,
children: Some(HashMap::new()),
},
);
nav.entries.get_mut(cat_name.as_str()).unwrap()
nav.entries.get_mut(cat.as_str()).unwrap()
}
};

View file

@ -7,9 +7,14 @@ mod lua;
mod parser;
use std::env;
use std::fs::OpenOptions;
use std::io::BufWriter;
use std::io::Write;
use std::process::ExitCode;
use std::rc::Rc;
use std::time::UNIX_EPOCH;
use compiler::compiler::CompiledDocument;
use compiler::compiler::Compiler;
use compiler::compiler::Target;
use compiler::navigation::create_navigation;
@ -18,6 +23,7 @@ use document::document::Document;
use getopts::Options;
use parser::langparser::LangParser;
use parser::parser::Parser;
use rusqlite::Connection;
use walkdir::WalkDir;
use crate::parser::source::SourceFile;
@ -42,38 +48,7 @@ NML version: 0.4\n"
);
}
fn compile(
target: Target,
doc: &Box<dyn Document>,
output: &String,
db_path: &Option<String>,
navigation: &Navigation,
multi_mode: bool,
) -> bool {
let compiler = Compiler::new(target, db_path.clone());
// Get output from file
if multi_mode {
let out_file = match doc.get_variable("compiler.output") {
None => {
eprintln!("Missing required variable `compiler.output` for multifile mode");
return false;
}
Some(var) => output.clone() + "/" + var.to_string().as_str(),
};
let out = compiler.compile(navigation, doc.as_ref());
std::fs::write(out_file, out).is_ok()
} else {
let out = compiler.compile(navigation, doc.as_ref());
std::fs::write(output, out).is_ok()
}
}
fn parse(
input: &String,
debug_opts: &Vec<String>,
) -> Result<Box<dyn Document<'static>>, String> {
fn parse(input: &String, debug_opts: &Vec<String>) -> Result<Box<dyn Document<'static>>, String> {
println!("Parsing {input}...");
let parser = LangParser::default();
@ -108,12 +83,69 @@ fn parse(
}
if parser.has_error() {
return Err("Parsing failed aborted due to errors while parsing".to_string())
return Err("Parsing failed aborted due to errors while parsing".to_string());
}
Ok(doc)
}
fn process(
target: Target,
files: Vec<String>,
db_path: &Option<String>,
debug_opts: &Vec<String>,
) -> Result<Vec<CompiledDocument>, String> {
let mut compiled = vec![];
let con = db_path
.as_ref()
.map_or(Connection::open_in_memory(), |path| Connection::open(path))
.map_err(|err| format!("Unable to open connection to the database: {err}"))?;
CompiledDocument::init_cache(&con)
.map_err(|err| format!("Failed to initialize cached document table: {err}"))?;
for file in files {
let meta = std::fs::metadata(&file)
.map_err(|err| format!("Failed to get metadata for `{file}`: {err}"))?;
let modified = meta
.modified()
.map_err(|err| format!("Unable to query modification time for `{file}`: {err}"))?;
let parse_and_compile = || -> Result<CompiledDocument, String> {
// Parse
let doc = parse(&file, debug_opts)?;
// Compile
let compiler = Compiler::new(target, db_path.clone());
let mut compiled = compiler.compile(&*doc);
// Insert into cache
compiled.mtime = modified.duration_since(UNIX_EPOCH).unwrap().as_secs();
compiled.insert_cache(&con).map_err(|err| {
format!("Failed to insert compiled document from `{file}` into cache: {err}")
})?;
Ok(compiled)
};
let cdoc = match CompiledDocument::from_cache(&con, &file) {
Some(compiled) => {
if compiled.mtime < modified.duration_since(UNIX_EPOCH).unwrap().as_secs() {
parse_and_compile()?
} else {
compiled
}
}
None => parse_and_compile()?,
};
compiled.push(cdoc);
}
Ok(compiled)
}
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
let program = args[0].clone();
@ -177,8 +209,7 @@ fn main() -> ExitCode {
let debug_opts = matches.opt_strs("z");
let db_path = matches.opt_str("d");
let mut docs = vec![];
let mut files = vec![];
if input_meta.is_dir() {
if db_path.is_none() {
eprintln!("Please specify a database (-d) for directory mode.");
@ -221,46 +252,53 @@ fn main() -> ExitCode {
continue;
}
match parse(&path, &debug_opts)
{
Ok(doc) => docs.push(doc),
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
}
files.push(path);
}
} else {
match parse(&input, &debug_opts)
{
Ok(doc) => docs.push(doc),
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
}
files.push(input);
}
// Build navigation
let navigation = match create_navigation(&docs)
{
Ok(nav) => nav,
// Parse, compile using the cache
let compiled = match process(Target::HTML, files, &db_path, &debug_opts) {
Ok(compiled) => compiled,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
println!("{navigation:#?}");
let multi_mode = input_meta.is_dir();
for doc in docs
{
if !compile(Target::HTML, &doc, &output, &db_path, &navigation, multi_mode)
{
eprintln!("Compilation failed, processing aborted");
// Build navigation
let navigation = match create_navigation(&compiled) {
Ok(nav) => nav,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
let compiled_navigation = navigation.compile(Target::HTML);
// Output
for doc in compiled {
let out_path = match doc
.get_variable("compiler.output")
.or(input_meta.is_file().then_some(&output))
{
Some(path) => path.clone(),
None => {
eprintln!("Unable to get output file for `{}`", doc.input);
return ExitCode::FAILURE;
}
};
let file = std::fs::File::create(output.clone() + "/" + out_path.as_str()).unwrap();
let mut writer = BufWriter::new(file);
write!(
writer,
"{}{}{}{}",
doc.header, compiled_navigation, doc.body, doc.footer
);
writer.flush();
}
return ExitCode::SUCCESS;