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