-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit b07b49b
Showing
22 changed files
with
1,262 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/target |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
[package] | ||
name = "webby" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
base64 = "0.22.1" | ||
boml = "0.3.1" | ||
|
||
[features] | ||
default = [] | ||
log = [] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
# webby | ||
|
||
> The smol web compiler | ||
As seen in [my website](https://github.com/bright-shard/website). | ||
|
||
**webby** is a small and efficient compiler for making static sites. It adds macros, minifiers, and translators to compile your project into a tiny static site. | ||
|
||
> Note: Webby is WIP. The above is a summary of what I want it to do when it's finished. For the current project status, see [todo](#todo). | ||
# macros | ||
|
||
webby adds a few simple macros to make writing HTML simpler. Macros open with `#!`, followed by the macro name, followed by arguments in parentheses, like so: | ||
|
||
``` | ||
#!MACRO_NAME(args) | ||
``` | ||
|
||
Macros can be combined, like this: | ||
|
||
``` | ||
#!MACRO1(#!MACRO2(args)) | ||
``` | ||
|
||
- `#!INCLUDE(path/to/file)`: Webby will compile the given file, then embed it at the macro's location. The file must contain valid UTF-8 text. | ||
- `#!BASE64(text)`: Base64-encode the given text. | ||
- `#!INCLUDE_BASE64(path/to/file)`: Base64-encode the given file. This differs from `#!BASE64(#!INCLUDE(path/to/file))` because it can also base64-encode binary files. | ||
|
||
# minifiers | ||
|
||
webby will automatically strip comments and unneeded whitespace from your code to make it as small as possible. | ||
|
||
# translators | ||
|
||
Translators cross-compile between languages - for example, Markdown to HTML, or Gemtext to HTML. | ||
|
||
|
||
|
||
# usage | ||
|
||
webby projects have a `webby.toml` in the root of their project, just like Rust projects have a `Cargo.toml` in the root of theirs. The format of `webby.toml` is given in [config](#config). | ||
|
||
To install webby, just install it with Cargo: | ||
|
||
```sh | ||
cargo install --git https://github.com/bright-shard/webby | ||
``` | ||
|
||
Then just run `webby` in your webby project. | ||
|
||
# config | ||
|
||
In its simplest form, the `webby.toml` file will look like this: | ||
|
||
```toml | ||
# For every file you want to compile with webby, add a `[[target]]` section | ||
[[target]] | ||
# The path to the file to compile | ||
path = "index.html" | ||
|
||
[[target]] | ||
path = "blog.html" | ||
``` | ||
|
||
However, webby allows customising more if you need it: | ||
|
||
```toml | ||
# (Optional) the directory to put the output files at | ||
# If this isn't specified it defaults to `webby` | ||
# The path is relative to the webby.toml file | ||
output = "my/custom/build/dir" | ||
|
||
[[target]] | ||
# The path to the file, relative to the webby.toml file | ||
# If you list a folder instead of a file, webby will compile all of the files | ||
# in that folder | ||
path = "path/to/file.html" | ||
# (Optional) Where to put the compiled file | ||
# If this isn't specified it defaults to the name of the file given in path | ||
# The path is relative to the output directory | ||
output = "file.out.html" | ||
# (Optional) The compilation mode | ||
# This can be "compile", "copy", or "link". Compile will compile the file. Copy | ||
# will just copy the file as-is and will not compile it at all. Link is the same | ||
# as copy, but it creates a hard link (not a symlink) to the file instead of | ||
# copying it. | ||
# If this isn't specified, webby will infer if it should compile or copy the | ||
# file based on the file's ending. | ||
mode = "compile" | ||
``` | ||
|
||
# todo | ||
|
||
- [x] Macros | ||
- [x] INCLUDE | ||
- [x] BASE64 | ||
- [x] BASE64_INCLUDE | ||
- [x] HTML minifier | ||
- [x] CSS minifier | ||
- [ ] JS minifier | ||
- [x] Gemtext translator | ||
- [ ] Markdown translator | ||
- [ ] Redo macro compiler... it's old and has bugs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
use { | ||
crate::{line_number_of_offset, Cow}, | ||
base64::{engine::general_purpose::STANDARD, Engine}, | ||
std::{fs, path::Path}, | ||
}; | ||
|
||
pub fn compile_macros<'a>(original: &'a str, source_path: &'a Path) -> Cow<'a> { | ||
let mut output = String::default(); | ||
let mut offset = 0; | ||
|
||
while let Some(start_idx) = original[offset..].find("#!") { | ||
if original[offset..] | ||
.as_bytes() | ||
.get(start_idx.saturating_sub(1)) | ||
.copied() | ||
== Some(b'\\') | ||
{ | ||
if !output.is_empty() { | ||
output += &original[offset..offset + start_idx + 1] | ||
} | ||
offset += start_idx + 1; | ||
continue; | ||
} | ||
|
||
output += &original[offset..offset + start_idx]; | ||
offset += start_idx; | ||
|
||
let macro_src = &original[offset..]; | ||
let paren_open = macro_src.find('(').unwrap_or_else(|| { | ||
panic!( | ||
"Expected ( in macro invocation at {source_path:?}:{}", | ||
line_number_of_offset(original, offset) | ||
) | ||
}); | ||
let mut paren_close = macro_src.find(')').unwrap_or_else(|| { | ||
panic!( | ||
"Expected ) to end macro invocation at {source_path:?}:{}", | ||
line_number_of_offset(original, offset) | ||
) | ||
}); | ||
while macro_src.as_bytes().get(paren_close + 1).copied() == Some(b')') { | ||
paren_close += 1; | ||
} | ||
|
||
let macro_name = ¯o_src[2..paren_open]; | ||
let macro_args = ¯o_src[paren_open + 1..paren_close]; | ||
let macro_args = compile_macros(macro_args, source_path); | ||
let macro_args = macro_args.as_ref(); | ||
|
||
match macro_name { | ||
"INCLUDE" => { | ||
let path = source_path.parent().unwrap().join(macro_args); | ||
let src = fs::read_to_string(&path).unwrap_or_else(|err| { | ||
panic!( | ||
"Error in INCLUDE macro at {source_path:?}:{}: {err}", | ||
line_number_of_offset(original, offset) | ||
) | ||
}); | ||
let compiled = compile_macros(&src, &path); | ||
output += compiled.as_ref(); | ||
} | ||
"BASE64" => { | ||
output += STANDARD.encode(macro_args).as_str(); | ||
} | ||
"INCLUDE_BASE64" => { | ||
let path = source_path.parent().unwrap().join(macro_args); | ||
let src = fs::read(&path).unwrap_or_else(|err| { | ||
panic!( | ||
"Error in INCLUDE_BASE64 macro at {source_path:?}:{}: {err}", | ||
line_number_of_offset(original, offset) | ||
) | ||
}); | ||
output += STANDARD.encode(&src).as_str(); | ||
} | ||
other => panic!( | ||
"Unknown macro '{other}' in macro invocation at {source_path:?}:{}", | ||
line_number_of_offset(original, offset) | ||
), | ||
} | ||
|
||
offset += paren_close + 1; | ||
} | ||
|
||
if output.is_empty() { | ||
Cow::Borrowed(original) | ||
} else { | ||
output += &original[offset..]; | ||
Cow::Owned(output) | ||
} | ||
} | ||
|
||
pub fn copy_batch_target(src: &Path, dest: &Path) { | ||
if dest.is_file() { | ||
fs::remove_file(dest).unwrap_or_else(|err| { | ||
panic!("Failed to copy batch target {src:?}. There was already a file where its output should go ({dest:?}), which couldn't be removed: {err}"); | ||
}); | ||
} | ||
if !dest.exists() { | ||
fs::create_dir_all(dest).unwrap_or_else(|err| { | ||
panic!("Failed to copy batch target {src:?}. Couldn't create its output folder at {dest:?} because: {err}"); | ||
}); | ||
} | ||
|
||
let src = src.read_dir().unwrap_or_else(|err| { | ||
panic!( | ||
"Failed to copy batch target {dest:?}. Couldn't open its source directory because: {err}" | ||
); | ||
}); | ||
|
||
for dir_entry in src.filter_map(|dir_entry| dir_entry.ok()) { | ||
let dir_entry = &dir_entry.path(); | ||
|
||
if dir_entry.is_file() { | ||
fs::copy(dir_entry, dest.join(dir_entry.file_name().unwrap())).unwrap_or_else(|err| { | ||
panic!("Failed to copy batch target {dest:?}. Couldn't copy file at {dir_entry:?} because: {err}"); | ||
}); | ||
} else { | ||
copy_batch_target(dir_entry, &dest.join(dir_entry.file_name().unwrap())); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
pub mod compiler; | ||
pub mod minifier; | ||
pub mod translator; | ||
|
||
use std::{fs, path::PathBuf}; | ||
|
||
type Cow<'a> = std::borrow::Cow<'a, str>; | ||
|
||
pub enum Mode { | ||
Compile, | ||
Copy, | ||
Link, | ||
} | ||
|
||
pub struct Target { | ||
pub path: PathBuf, | ||
pub output: PathBuf, | ||
pub mode: Mode, | ||
} | ||
|
||
pub fn build_target(target: Target) -> Result<(), Cow<'static>> { | ||
match target.mode { | ||
Mode::Copy => { | ||
if target.path.is_file() | target.path.is_symlink() { | ||
fs::copy(target.path, target.output).unwrap(); | ||
} else { | ||
compiler::copy_batch_target(&target.path, &target.output); | ||
} | ||
} | ||
Mode::Link => { | ||
if target.output.exists() { | ||
fs::remove_file(&target.output) | ||
.unwrap_or_else(|err| panic!("Failed to link target {:?}: {err}", &target.path)) | ||
} | ||
fs::hard_link(&target.path, target.output) | ||
.unwrap_or_else(|err| panic!("Failed to link target {:?}: {err}", &target.path)); | ||
} | ||
Mode::Compile => { | ||
let original = fs::read_to_string(&target.path).unwrap_or_else(|err| { | ||
panic!( | ||
"Failed to compile target {:?}: Error occurred while reading the source file: {err}", | ||
&target.path | ||
) | ||
}); | ||
let compiled_macros = compiler::compile_macros(&original, &target.path); | ||
|
||
let output = match target.path.extension().and_then(|val| val.to_str()) { | ||
Some("gmi") => Cow::Owned(translator::translate_gemtext( | ||
&target.path, | ||
compiled_macros.as_ref(), | ||
)?), | ||
Some("html") => Cow::Owned(minifier::minify_html( | ||
target.path.to_str().unwrap(), | ||
&compiled_macros, | ||
&original, | ||
)?), | ||
Some("css") => Cow::Owned(minifier::minify_css(&compiled_macros)), | ||
_ => compiled_macros, | ||
}; | ||
|
||
fs::write(&target.output, output.as_ref()) | ||
.unwrap_or_else(|err| panic!("Failed to compile target {:?}: Error occured while writing the compiled file: {err}", &target.path)); | ||
} | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
fn line_number_of_offset(src: &str, offset: usize) -> usize { | ||
src[..offset].bytes().filter(|byte| *byte == b'\n').count() | ||
} |
Oops, something went wrong.