initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+1206
File diff suppressed because it is too large
Load Diff
+12
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "template-compiler"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
indoc = "2"
|
||||
lightningcss = "1.0.0-alpha.71"
|
||||
rand = "0.8"
|
||||
minify-js = "0.6.0"
|
||||
@@ -0,0 +1,10 @@
|
||||
# FastCentrik custom template compiler
|
||||
|
||||
This is a custom template compiler for FastCentrik. It allows you to create templates with and compile them into injectable JavaScript.
|
||||
|
||||
## Usage
|
||||
|
||||
// Work in progress
|
||||
|
||||
## TODO
|
||||
- [ ] Add option to disable the default styles
|
||||
@@ -0,0 +1,135 @@
|
||||
use std::fs;
|
||||
|
||||
// Make sure your models match your struct definitions
|
||||
use crate::compiler::models::{Page, PageRoute, Project, Template};
|
||||
|
||||
pub fn compile() {
|
||||
println!("Compiling project...");
|
||||
|
||||
let page_paths = list_all_paths();
|
||||
|
||||
let mut pages = Vec::new();
|
||||
for path in page_paths {
|
||||
let page = construct_page(&path);
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
let project = Project { pages };
|
||||
let compiled_script = project.compile_script();
|
||||
fs::write("compiled.js", compiled_script).expect("Failed to write compiled script");
|
||||
}
|
||||
|
||||
fn list_all_paths() -> Vec<String> {
|
||||
let mut paths = Vec::new();
|
||||
let entries = fs::read_dir(".").expect("Failed to read current directory");
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
let dir_name = path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
if !dir_name.starts_with('.') && dir_name != "target" {
|
||||
paths.push(path.to_str().unwrap().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
fn construct_page(page_path: &str) -> Page {
|
||||
// --- 1. Route Resolution ---
|
||||
let suffixes = [".regexpr", ".js", ""];
|
||||
let mut route_option: Option<PageRoute> = None;
|
||||
|
||||
for suffix in suffixes {
|
||||
let file_path = format!("{}/route{}", page_path, suffix);
|
||||
|
||||
if let Ok(content) = fs::read_to_string(&file_path) {
|
||||
route_option = Some(match suffix {
|
||||
".regexpr" => PageRoute::Regexpr(content),
|
||||
".js" => PageRoute::CustomScript(content),
|
||||
"" => PageRoute::Static(content),
|
||||
_ => unreachable!(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let route = route_option.unwrap_or_else(|| {
|
||||
panic!("No valid route file found (route.regexpr, route.js, or route) in '{}'", page_path)
|
||||
});
|
||||
|
||||
// --- 2. Page-level Style ---
|
||||
let style_path = format!("{}/style.css", page_path);
|
||||
let style = fs::read_to_string(&style_path).ok();
|
||||
|
||||
// --- 3. Parse Templates ---
|
||||
let mut content = Vec::new();
|
||||
let templates_dir = format!("{}/templates", page_path);
|
||||
|
||||
// Check if the templates directory exists for this page
|
||||
if let Ok(entries) = fs::read_dir(&templates_dir) {
|
||||
for entry in entries {
|
||||
let entry = entry.expect("Failed to read template entry");
|
||||
let path = entry.path();
|
||||
|
||||
// Only process folders inside the 'templates' directory
|
||||
if path.is_dir() {
|
||||
let template_path = path.to_str().unwrap();
|
||||
let template = construct_template(template_path);
|
||||
content.push(template);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Page {
|
||||
route,
|
||||
style,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
fn construct_template(template_path: &str) -> Template {
|
||||
// --- 1. Template-level Style ---
|
||||
let style_path = format!("{}/style.css", template_path);
|
||||
let style = fs::read_to_string(&style_path).ok();
|
||||
|
||||
// --- 2. Template Content Resolution ---
|
||||
let script_path = format!("{}/script.js", template_path);
|
||||
let template_html_path = format!("{}/template.html", template_path);
|
||||
|
||||
if let Ok(script_content) = fs::read_to_string(&script_path) {
|
||||
Template::CustomScript {
|
||||
script: script_content,
|
||||
style,
|
||||
}
|
||||
}
|
||||
else if let Ok(template_content) = fs::read_to_string(&template_html_path) {
|
||||
let scraper_path = format!("{}/scrape.js", template_path);
|
||||
let scraper = fs::read_to_string(&scraper_path).ok();
|
||||
|
||||
let replace_selector_path = format!("{}/replace_selector", template_path);
|
||||
let replace_selector = fs::read_to_string(&replace_selector_path).unwrap_or_else(|_| {
|
||||
panic!("replace_selector file not found in '{}': Required for template injection", template_path);
|
||||
});
|
||||
|
||||
Template::TemplateInjector {
|
||||
template: template_content,
|
||||
// .trim() prevents trailing newlines in text files from breaking CSS selectors
|
||||
replace_selector: replace_selector.trim().to_string(),
|
||||
scraper,
|
||||
style,
|
||||
}
|
||||
}
|
||||
else {
|
||||
panic!(
|
||||
"Template content not found in '{}': Directory must contain either script.js or template.html",
|
||||
template_path
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod compiler;
|
||||
pub mod models;
|
||||
@@ -0,0 +1,37 @@
|
||||
pub struct Project {
|
||||
pub pages: Vec<Page>,
|
||||
}
|
||||
|
||||
pub struct Page {
|
||||
pub route: PageRoute,
|
||||
pub style: Option<String>,
|
||||
pub content: Vec<Template>,
|
||||
}
|
||||
|
||||
pub enum PageRoute {
|
||||
/// File `route.regexpr`
|
||||
Regexpr(String),
|
||||
|
||||
/// File `route`
|
||||
Static(String),
|
||||
|
||||
/// File `route.js`
|
||||
CustomScript(String),
|
||||
}
|
||||
|
||||
pub enum Template {
|
||||
CustomScript {
|
||||
script: String,
|
||||
style: Option<String>,
|
||||
},
|
||||
|
||||
TemplateInjector {
|
||||
template: String,
|
||||
replace_selector: String,
|
||||
scraper: Option<String>,
|
||||
style: Option<String>
|
||||
},
|
||||
}
|
||||
|
||||
#[path = "models_impl.rs"]
|
||||
mod models_impl;
|
||||
@@ -0,0 +1,168 @@
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::css::scope_css;
|
||||
|
||||
use crate::compiler::models::{PageRoute, Project, Template};
|
||||
use crate::js::minify_javascript;
|
||||
|
||||
impl Project {
|
||||
pub fn compile_script(&self) -> String {
|
||||
let template = indoc! { r#"
|
||||
(function() {
|
||||
if (typeof Handlebars === 'undefined') {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.js';
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
const tempDefine = window.define;
|
||||
window.define = undefined;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.js';
|
||||
|
||||
script.onload = function() {
|
||||
window.define = tempDefine;
|
||||
|
||||
{pages_script}
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
})()
|
||||
"# };
|
||||
let mut script = String::new();
|
||||
|
||||
for page in &self.pages {
|
||||
let template_script = indoc! { r#"
|
||||
if (eval({page_route})) {
|
||||
{content_script}
|
||||
}
|
||||
"# };
|
||||
|
||||
let route_script = page.route.as_script();
|
||||
let content_script = page
|
||||
.content
|
||||
.iter()
|
||||
.map(|template| template.as_script())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
script.push_str(
|
||||
&template_script
|
||||
.replace("{page_route}", &format!("{:?}", minify_javascript(&route_script)))
|
||||
.replace("{content_script}", &content_script),
|
||||
);
|
||||
}
|
||||
|
||||
minify_javascript(&template.replace("{pages_script}", &script))
|
||||
}
|
||||
}
|
||||
|
||||
impl PageRoute {
|
||||
fn as_script(&self) -> String {
|
||||
match self {
|
||||
PageRoute::Regexpr(expr) => {
|
||||
let template = indoc! { r#"
|
||||
(function() {
|
||||
const regex = new RegExp({expr});
|
||||
return regex.test(window.location.pathname);
|
||||
})()
|
||||
"# };
|
||||
|
||||
template.replace("{expr}", &format!("{:?}", expr.trim()))
|
||||
}
|
||||
PageRoute::Static(route) => {
|
||||
let template = indoc! { r#"
|
||||
(function() {
|
||||
return window.location.pathname === {route};
|
||||
})()
|
||||
"# };
|
||||
|
||||
template.replace("{route}", &format!("{:?}", route))
|
||||
}
|
||||
PageRoute::CustomScript(script) => script.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Template {
|
||||
pub fn as_script(&self) -> String {
|
||||
match self {
|
||||
Template::CustomScript { script, style } => {
|
||||
if style.is_none() || style.as_ref().unwrap().trim().is_empty() {
|
||||
return script.clone();
|
||||
}
|
||||
|
||||
let style_script = style.as_ref().map_or(String::new(), |s| {
|
||||
format!(
|
||||
r#"
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = {style};
|
||||
document.head.appendChild(styleElement);
|
||||
"#,
|
||||
style = format!("{:?}", s)
|
||||
)
|
||||
});
|
||||
|
||||
format!("{}\n{}", style_script, script)
|
||||
}
|
||||
Template::TemplateInjector {
|
||||
template,
|
||||
replace_selector,
|
||||
scraper,
|
||||
style,
|
||||
} => {
|
||||
let scope_class = format!("template-style-{}", rand::random::<u32>());
|
||||
|
||||
let style_script = if let Some(style) = style {
|
||||
if style.trim().is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(
|
||||
indoc! {r#"
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = {style};
|
||||
document.head.appendChild(styleElement);
|
||||
"# },
|
||||
style = &scope_css(&style, &scope_class)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let script_template = indoc! { r#"
|
||||
(function() {
|
||||
{style_script}
|
||||
|
||||
const parsedData = eval({scraper} || "(() => ({}))");
|
||||
const template = Handlebars.compile({template});
|
||||
const rendered = template(parsedData);
|
||||
console.log("Rendered template:", rendered);
|
||||
const targetElement = document.querySelector({replace_selector});
|
||||
|
||||
const temp = document.createElement('template');
|
||||
temp.innerHTML = rendered;
|
||||
|
||||
const newElement = temp.content.firstElementChild;
|
||||
|
||||
targetElement.replaceWith(newElement);
|
||||
|
||||
newElement.classList.add({scope_class});
|
||||
})();
|
||||
"# };
|
||||
|
||||
script_template
|
||||
.replace("{style_script}", &style_script)
|
||||
.replace("{template}", &format!("{:?}", template.trim()))
|
||||
.replace("{replace_selector}", &format!("{:?}", replace_selector))
|
||||
.replace("{scope_class}", &format!("{:?}", scope_class))
|
||||
.replace(
|
||||
"{scraper}",
|
||||
&format!("{:?}", minify_javascript(scraper.as_deref().unwrap_or(""))),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
use lightningcss::{
|
||||
stylesheet::{ParserFlags, ParserOptions, PrinterOptions, StyleSheet},
|
||||
targets::{Browsers, Targets},
|
||||
};
|
||||
|
||||
pub fn scope_css(raw_css: &str, scope_class: &str) -> String {
|
||||
let nested_css = format!(".{} {{\n{}\n}}", scope_class, raw_css);
|
||||
|
||||
let stylesheet = StyleSheet::parse(
|
||||
&nested_css,
|
||||
ParserOptions {
|
||||
flags: ParserFlags::NESTING,
|
||||
..ParserOptions::default()
|
||||
},
|
||||
)
|
||||
.expect("Failed to parse CSS");
|
||||
|
||||
let targets = Targets {
|
||||
browsers: Some(Browsers {
|
||||
// Chrome 90 predates native CSS nesting support
|
||||
chrome: Some(90 << 16),
|
||||
..Browsers::default()
|
||||
}),
|
||||
..Targets::default()
|
||||
};
|
||||
|
||||
let printed = stylesheet
|
||||
.to_css(PrinterOptions {
|
||||
minify: true,
|
||||
targets,
|
||||
..PrinterOptions::default()
|
||||
})
|
||||
.expect("Failed to serialize CSS");
|
||||
|
||||
printed.code
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use minify_js::{minify, Session, TopLevelMode};
|
||||
|
||||
pub fn minify_javascript(raw_js: &str) -> String {
|
||||
let session = Session::new();
|
||||
let mut out = Vec::new();
|
||||
|
||||
minify(
|
||||
&session,
|
||||
TopLevelMode::Global,
|
||||
raw_js.as_bytes(),
|
||||
&mut out,
|
||||
)
|
||||
.expect("Failed to minify JavaScript");
|
||||
|
||||
String::from_utf8(out).expect("Minifier produced invalid UTF-8")
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
use clap::{ArgGroup, Parser};
|
||||
|
||||
use crate::compiler::compiler::compile;
|
||||
|
||||
mod compiler;
|
||||
|
||||
pub mod css;
|
||||
pub mod js;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "template-compiler",
|
||||
version = env!("CARGO_PKG_VERSION"),
|
||||
about = "Template compiler for FastCentrik templates.",
|
||||
author = "Jakub Žitník",
|
||||
group = ArgGroup::new("command")
|
||||
.args(&["new", "compile"])
|
||||
.multiple(false)
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(short = 'n', long = "new", help = "Lists all the saved mount points.")]
|
||||
new: bool,
|
||||
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "compile",
|
||||
help = "Compiles the project into single JS file.",
|
||||
value_name = "NAME"
|
||||
)]
|
||||
compile: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.compile {
|
||||
compile();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user